Skip to content

Commit dda82e8

Browse files
committed
chore: add email
1 parent 3147e0b commit dda82e8

11 files changed

+637
-2
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ testem.log
4040
.DS_Store
4141
Thumbs.db
4242

43+
*.tgz
4344
apps/**/webpack.config.js

apps/demo/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"nativescript-theme-core": "file:../../node_modules/nativescript-theme-core",
1414
"@nativescript/core": "file:../../node_modules/@nativescript/core",
1515
"@nativescript/local-notifications": "file:../../packages/local-notifications",
16-
"@nativescript/shared-notification-delegate": "file:../../packages/shared-notification-delegate"
16+
"@nativescript/shared-notification-delegate": "file:../../packages/shared-notification-delegate",
17+
"@nativescript/email": "file:../../packages/email"
1718
},
1819
"devDependencies": {
1920
"@nativescript/webpack": "~2.1.0",

apps/demo/src/main-page.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<StackLayout class="p-20">
77
<ScrollView class="h-full">
88
<StackLayout>
9-
<Button text="local-notifications" tap="{{ viewDemo }}" class="btn"/>
9+
<Button text="local-notifications" tap="{{ viewDemo }}" class="btn view-demo"/>
1010

1111
</StackLayout>
1212
</ScrollView>

nx.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@
3535
},
3636
"demo": {
3737
"tags": []
38+
},
39+
"email": {
40+
"tags": []
3841
}
3942
},
4043
"workspaceLayout": {

packages/email/README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# NativeScript Email
2+
3+
[![NPM version][npm-image]][npm-url]
4+
[![Downloads][downloads-image]][npm-url]
5+
[![Twitter Follow][twitter-image]][twitter-url]
6+
7+
[npm-image]:http://img.shields.io/npm/v/nativescript-email.svg
8+
[npm-url]:https://npmjs.org/package/nativescript-email
9+
[downloads-image]:http://img.shields.io/npm/dm/nativescript-email.svg
10+
[twitter-image]:https://img.shields.io/twitter/follow/eddyverbruggen.svg?style=social&label=Follow%20me
11+
[twitter-url]:https://twitter.com/eddyverbruggen
12+
13+
You can use this plugin to compose an e-mail, have the user edit the draft manually, and send it.
14+
15+
> Note that this plugin depends on the default mail app. If you want a fallback to a third party client app like Gmail or Outlook, then check for availability, and if not available use a solution like [the Social Share plugin](https://github.com/tjvantoll/nativescript-social-share).
16+
17+
```javascript
18+
tns plugin add @nativescript/email
19+
```
20+
21+
## Usage
22+
23+
## API
24+
25+
To use this plugin you must first require/import it:
26+
27+
#### TypeScript
28+
29+
```typescript
30+
import * as email from "@nativescript/email";
31+
// or
32+
import { compose } from "@nativescript/email";
33+
// or even
34+
import { compose as composeEmail } from "@nativescript/email";
35+
```
36+
37+
#### JavaScript
38+
39+
```js
40+
var email = require("@nativescript/email");
41+
```
42+
43+
### `available`
44+
45+
#### TypeScript
46+
47+
```typescript
48+
email.available().then((avail: boolean) => {
49+
console.log("Email available? " + avail);
50+
})
51+
```
52+
53+
#### JavaScript
54+
55+
```js
56+
email.available().then(function(avail) {
57+
console.log("Email available? " + avail);
58+
})
59+
```
60+
61+
### `compose`
62+
63+
#### JavaScript
64+
65+
```js
66+
// let's first create a File object using the tns file module
67+
var fs = require("file-system");
68+
var appPath = fs.knownFolders.currentApp().path;
69+
var logoPath = appPath + "/res/telerik-logo.png";
70+
71+
email.compose({
72+
subject: "Yo",
73+
body: "Hello <strong>dude</strong> :)",
74+
to: ['eddyverbruggen@gmail.com', 'to@person2.com'],
75+
cc: ['ccperson@somewhere.com'],
76+
bcc: ['eddy@combidesk.com', 'eddy@x-services.nl'],
77+
attachments: [
78+
{
79+
fileName: 'arrow1.png',
80+
path: 'base64://iVBORw0KGgoAAAANSUhEUgAAABYAAAAoCAYAAAD6xArmAAAACXBIWXMAABYlAAAWJQFJUiTwAAAAHGlET1QAAAACAAAAAAAAABQAAAAoAAAAFAAAABQAAAB5EsHiAAAAAEVJREFUSA1iYKAimDhxYjwIU9FIBgaQgZMmTfoPwlOmTJGniuHIhlLNxaOGwiNqNEypkwlGk9RokoIUfaM5ijo5Clh9AAAAAP//ksWFvgAAAEFJREFUY5g4cWL8pEmT/oMwiM1ATTBqONbQHA2W0WDBGgJYBUdTy2iwYA0BrILDI7VMmTJFHqv3yBUEBQsIg/QDAJNpcv6v+k1ZAAAAAElFTkSuQmCC',
81+
mimeType: 'image/png'
82+
},
83+
{
84+
fileName: 'telerik-logo.png',
85+
path: logoPath,
86+
mimeType: 'image/png'
87+
}]
88+
}).then(
89+
function() {
90+
console.log("Email composer closed");
91+
}, function(err) {
92+
console.log("Error: " + err);
93+
});
94+
```
95+
96+
Full attachment support has been added to 1.3.0 per the example above.
97+
98+
Since 1.4.0 the promise will be rejected in case a file can't be found.
99+
100+
## Usage with Angular
101+
Check out [this tutorial (YouTube)](https://www.youtube.com/watch?v=fSnQb9-Gtdk) to learn how to use this plugin in a NativeScript-Angular app.
102+
103+
## Known issues
104+
On iOS you can't use the simulator to test the plugin because of an iOS limitation.
105+
To prevent a crash this plugin returns `false` when `available` is invoked on the iOS sim.
106+
107+
108+
109+
## License
110+
111+
Apache License Version 2.0

packages/email/index.android.ts

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { Application, File, Folder } from "@nativescript/core";
2+
3+
(function () {
4+
_cleanAttachmentFolder();
5+
})();
6+
7+
const _determineAvailability = function () {
8+
const uri = android.net.Uri.fromParts("mailto", "", null);
9+
const intent = new android.content.Intent(android.content.Intent.ACTION_SENDTO, uri);
10+
const packageManager = Application.android.context.getPackageManager();
11+
const nrOfMailApps = packageManager.queryIntentActivities(intent, 0).size();
12+
return nrOfMailApps > 0;
13+
};
14+
15+
export function available() {
16+
return new Promise(function (resolve, reject) {
17+
try {
18+
resolve(_determineAvailability());
19+
} catch (ex) {
20+
console.log("Error in email.available: " + ex);
21+
reject(ex);
22+
}
23+
});
24+
};
25+
26+
export function compose(arg) {
27+
return new Promise(function (resolve, reject) {
28+
try {
29+
30+
if (!_determineAvailability()) {
31+
reject("No mail available");
32+
}
33+
34+
const mail = new android.content.Intent(android.content.Intent.ACTION_SENDTO);
35+
if (arg.body) {
36+
const htmlPattern = java.util.regex.Pattern.compile(".*\\<[^>]+>.*", java.util.regex.Pattern.DOTALL);
37+
if (htmlPattern.matcher(arg.body).matches()) {
38+
mail.putExtra(android.content.Intent.EXTRA_TEXT, android.text.Html.fromHtml(arg.body));
39+
mail.setType("text/html");
40+
} else {
41+
mail.putExtra(android.content.Intent.EXTRA_TEXT, arg.body);
42+
mail.setType("text/plain");
43+
}
44+
}
45+
46+
if (arg.subject) {
47+
mail.putExtra(android.content.Intent.EXTRA_SUBJECT, arg.subject);
48+
}
49+
if (arg.to) {
50+
mail.putExtra(android.content.Intent.EXTRA_EMAIL, toStringArray(arg.to));
51+
}
52+
if (arg.cc) {
53+
mail.putExtra(android.content.Intent.EXTRA_CC, toStringArray(arg.cc));
54+
}
55+
if (arg.bcc) {
56+
mail.putExtra(android.content.Intent.EXTRA_BCC, toStringArray(arg.bcc));
57+
}
58+
59+
if (arg.attachments) {
60+
const uris = new java.util.ArrayList();
61+
for (const a in arg.attachments) {
62+
const attachment = arg.attachments[a];
63+
const path = attachment.path;
64+
const fileName = attachment.fileName;
65+
const uri = _getUriForPath(path, fileName, Application.android.context);
66+
67+
if (!uri) {
68+
reject("File not found for path: " + path);
69+
return;
70+
}
71+
uris.add(uri);
72+
}
73+
74+
if (!uris.isEmpty()) {
75+
// required for Android 7+ (alternative is using a FileProvider (which is a better solution btw))
76+
const builder = new android.os.StrictMode.VmPolicy.Builder();
77+
android.os.StrictMode.setVmPolicy(builder.build());
78+
79+
mail.setAction(android.content.Intent.ACTION_SEND_MULTIPLE);
80+
mail.setType("message/rfc822");
81+
mail.putParcelableArrayListExtra(android.content.Intent.EXTRA_STREAM, uris);
82+
}
83+
} else {
84+
mail.setData(android.net.Uri.parse("mailto:"));
85+
}
86+
87+
mail.setFlags(android.content.Intent.FLAG_ACTIVITY_NEW_TASK);
88+
89+
// we can wire up an intent receiver but it's always the same resultCode (0, canceled) anyway
90+
Application.android.context.startActivity(mail);
91+
resolve(true);
92+
} catch (ex) {
93+
console.log("Error in email.compose: " + ex);
94+
reject(ex);
95+
}
96+
});
97+
};
98+
99+
function _getUriForPath(path, fileName, ctx) {
100+
if (path.indexOf("file:///") === 0) {
101+
return _getUriForAbsolutePath(path);
102+
} else if (path.indexOf("file://") === 0) {
103+
return _getUriForAssetPath(path, fileName, ctx);
104+
} else if (path.indexOf("base64:") === 0) {
105+
return _getUriForBase64Content(path, fileName, ctx);
106+
} else {
107+
if (path.indexOf(ctx.getPackageName()) > -1) {
108+
return _getUriForAssetPath(path, fileName, ctx);
109+
} else {
110+
return _getUriForAbsolutePath(path);
111+
}
112+
}
113+
}
114+
115+
function _getUriForAbsolutePath(path) {
116+
const absPath = path.replace("file://", "");
117+
const file = new java.io.File(absPath);
118+
if (!file.exists()) {
119+
console.log("File not found: " + file.getAbsolutePath());
120+
return null;
121+
} else {
122+
return android.net.Uri.fromFile(file);
123+
}
124+
}
125+
126+
function _getUriForAssetPath(path, fileName, ctx) {
127+
path = path.replace("file://", "/");
128+
if (!File.exists(path)) {
129+
console.log("File does not exist: " + path);
130+
return null;
131+
}
132+
133+
const localFile = File.fromPath(path);
134+
const localFileContents = localFile.readSync(function (e) {
135+
console.log('read file error:', e);
136+
});
137+
138+
let cacheFileName = _writeBytesToFile(ctx, fileName, localFileContents);
139+
if (cacheFileName.indexOf("file://") === -1) {
140+
cacheFileName = "file://" + cacheFileName;
141+
}
142+
return android.net.Uri.parse(cacheFileName);
143+
}
144+
145+
function _getUriForBase64Content(path, fileName, ctx) {
146+
const resData = path.substring(path.indexOf("://") + 3);
147+
let bytes;
148+
try {
149+
bytes = android.util.Base64.decode(resData, 0);
150+
} catch (ex) {
151+
console.log("Invalid Base64 string: " + resData);
152+
return android.net.Uri.EMPTY;
153+
}
154+
const cacheFileName = _writeBytesToFile(ctx, fileName, bytes);
155+
156+
return android.net.Uri.parse(cacheFileName);
157+
}
158+
159+
function _writeBytesToFile(ctx, fileName, contents) {
160+
const dir = ctx.getExternalCacheDir();
161+
162+
if (dir === null) {
163+
console.log("Missing external cache dir");
164+
return null;
165+
}
166+
167+
const storage = dir.toString() + "/emailcomposer";
168+
let cacheFileName = storage + "/" + fileName;
169+
170+
const toFile = File.fromPath(cacheFileName);
171+
toFile.writeSync(contents, function (e) {
172+
console.log('write file error:', e);
173+
});
174+
175+
if (cacheFileName.indexOf("file://") === -1) {
176+
cacheFileName = "file://" + cacheFileName;
177+
}
178+
return cacheFileName;
179+
}
180+
181+
function _cleanAttachmentFolder() {
182+
183+
if (Application.android.context) {
184+
const dir = Application.android.context.getExternalCacheDir();
185+
186+
if (dir === null) {
187+
console.log("Missing external cache dir");
188+
return;
189+
}
190+
191+
const storage = dir.toString() + "/emailcomposer";
192+
const cacheFolder = Folder.fromPath(storage);
193+
cacheFolder.clear();
194+
}
195+
}
196+
197+
const toStringArray = function (arg) {
198+
const arr = java.lang.reflect.Array.newInstance(java.lang.String.class, arg.length);
199+
for (let i = 0; i < arg.length; i++) {
200+
arr[i] = arg[i];
201+
}
202+
return arr;
203+
};

0 commit comments

Comments
 (0)