├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── plugin.xml ├── src ├── android │ ├── AssetUtil.java │ ├── EmailComposer.java │ ├── Impl.java │ ├── Provider.java │ └── xml │ │ └── emailcomposer_provider_paths.xml ├── browser │ └── EmailComposerProxy.js ├── ios │ ├── APPEmailComposer.h │ ├── APPEmailComposer.m │ ├── APPEmailComposerImpl.h │ └── APPEmailComposerImpl.m ├── osx │ ├── APPEmailComposer.h │ ├── APPEmailComposer.m │ ├── APPEmailComposerImpl.h │ └── APPEmailComposerImpl.m └── windows │ ├── EmailComposerProxy.js │ └── EmailComposerProxyImpl.js ├── test └── readme.md └── www └── email_composer.js /.gitignore: -------------------------------------------------------------------------------- 1 | /platforms 2 | /out -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | ## ChangeLog 3 | 4 | #### Version 0.9.2 (24.01.2019) 5 | - Fix package android.support.v4.content does not exist 6 | 7 | #### Version 0.9.1 (13.12.2018) 8 | - Fix line breaks (\r\n) on Android 9 | 10 | #### Version 0.9.0 (13.12.2018) 11 | - [feature:] Added `getClients` that returns a list of available email clients (Android) 12 | - [__change__]: Replace `isAvailable` through `hasClient` and `hasAccount`. 13 | - [__change__]: Plugin does not add any permissions by itself like `GET_ACCOUNTS` or `READ_EXTERNAL_STORAGE`. 14 | - [__change__]: `isAvailable` does not request for missing permission (`GET_ACCOUNTS`). 15 | - [__change__]: `hasPermission` takes 3 arguments now. The first one has to be a value of `cordova.plugins.email.permission`. 16 | - [__change__]: `requestPermission` takes 3 arguments now. The first one has to be a value of `cordova.plugins.email.permission`. 17 | - [__change__]: Remove support lib from being installed (Android). 18 | - [__change__]: Remove deprecated namespace `plugin.email`. 19 | - [__change__]: Remove deprecated support for `isHTML`. 20 | - [__change__]: Change default value for `isHtml` to `false`. 21 | - [__change__]: Remove Android specific `type` property. 22 | - [enhancement]: Skip chooser if there's only the default app (#302) 23 | - [enhancement]: Improve chooser to only display email clients. 24 | - [enhancement]: Add `from` to specify the sending email account. 25 | - [bugfix:] Do not open old email draft [fixes #303] 26 | 27 | #### Version 0.8.15 (08.02.2018) 28 | - Fix iOS not working if `app:` wasn't specified. 29 | - Fix `attachments:` to accept a string. 30 | 31 | #### Version 0.8.14 (31.01.2018) 32 | - Fix wrong uri encoding for browser platform. 33 | 34 | #### Version 0.8.13 (25.01.2018) 35 | - Fix potential wrong result for isAvailable on iOS+OSX by using scheme other then `mailto:` 36 | - Fix open app from background thread by using scheme other then `mailto:` 37 | 38 | #### Version 0.8.12 (09.01.2018) 39 | - Internal code refactoring 40 | - Added `type` property to specify the content type (#283) 41 | 42 | #### Version 0.8.11 (25.10.2017) 43 | - Apply URL encoding when constructing mailto: link (#273) 44 | 45 | #### Version 0.8.10 (25.09.2017) 46 | - Open gmail on ios and macos [fixes #272] 47 | - Added alias for outlook 48 | - Fix warnings with iOS 11 49 | 50 | #### Version 0.8.9 (14.09.2017) 51 | - Fix opening email with file attachment causes app crash on Android 8 (#270) 52 | 53 | #### Version 0.8.8 (07.09.2017) 54 | - Fix crash on iOS if attachment could not be found 55 | - Fix wrong plugin ID in package.json 56 | 57 | #### Version 0.8.7 (26.06.2017) 58 | - Add support for an app:// URL #158 (Android) 59 | - Add support for an app:// (iOS, OSX, Windows) 60 | 61 | #### Version 0.8.6 (12.06.2017) 62 | - Fixed issue with Android 4.x 63 | 64 | #### Version 0.8.5 (09.06.2017) 65 | 10 commits including bug fixes and enhancements: 66 | - [enhancement]: Support for osx platform 67 | - [enhancement]: Added `isAvailable2` which works equal except the callback args are in reverse order. 68 | - [enhancement]: Fixed possible attachment issues some Android email clients. 69 | 70 | #### Version 0.8.4 (06.06.2017) 71 | 25 commits including bug fixes and enhancements: 72 | - [__change__]: Skip availability checks with `email.open()` 73 | - [__change__]: Upgrade minimum required engine versions 74 | - [enhancement]: Treat callback functions as optional 75 | - [enhancement]: Support for Android API 23 Permission API 76 | - [enhancement]: Test the account name if they match the email pattern (#180) 77 | - [enhancement]: Support newest cordova platform versions 78 | - [enhancement]: Use @synthesize to prevent EXC_BAD_ACCESS errors with non-ARC code (#207) 79 | - [bugfix]: res:// uri not resolved on cordova-android@6 (6334d0) 80 | - [bugfix]: Require old plugin id for windows platform (#176) 81 | - [bugfix]: Memory leak for iOS 82 | 83 | #### Version 0.8.3 (01.03.2016) 84 | 63 commits including bug fixes and enhancements: 85 | - [___change:___] New plugin ID: _cordova-plugin-email-composer_ 86 | - [enhancement:] Published on npm 87 | - [enhancement:] Allowed the chooser header text to be configured (#113) 88 | - [enhancement:] Plain mailto: support 89 | - [enhancement:] Specify email client using `app:` flag 90 | - [enhancement:] More samples in Sample-App 91 | - [bugfix:] Build issues with iOS and Android 92 | - [bugfix:] Compatibility with newest OS and cordova platform versions 93 | - [bugfix:] Crash on iOS when presenting view controller from background (#169) 94 | - [bugfix:] Crash on iOS when no email account is setup 95 | - [bugfix:] Resolved issues with attachments on all platforms 96 | - ... 97 | 98 | #### Version 0.8.2 (01.03.2015) 99 | - Added new namespace `cordova.plugins.email`
100 | **Note:** The former `plugin.email` namespace is now deprecated and will be removed with the next major release. 101 | - [___change:___] Unified `absolute:` and `relative:` to `file:` 102 | - [___change:___] Renamed `isServiceAvailable` to `isAvailable` 103 | - [feature:] `app:` allows to specify target mail app on Android 104 | - [feature:] `res:` prefix for native ressource attachments 105 | - [enhancement:] Support attachments on Windows Phone 8.1 106 | - [enhancement:] `open` supports callbacks 107 | - [enhancement:] `isHTML` can be used next `isHtml` 108 | - [enhancement:] Set mime type to binary if unknown 109 | - [bugfix:] Defaults were ignored 110 | 111 | #### Version 0.8.1 (06.04.2014) 112 | - [enhancement:] Make use Cordovas NSData+Base64 extension. 113 | - [enhancement:] Log error message if attachment path does not exist. 114 | - [feature:] Add support for amazon fire 115 | - [bugfix:] Fix INSTALL_FAILED_CONFLICTING_PROVIDER error 116 | - [bugfix:] `relative://` attachment path wasnt working due to a missing permission. 117 | - [bugfix:] `base64://` attachment path looked up in the wrong directory. 118 | - [enhancement:] `relative://` supports now any file types and not only images. 119 | - [___change:___] `relative://` URI's even for Android need a file extension. 120 | 121 | #### Version 0.8.0 (02.03.2014) 122 | - [enhancement:] New `absolute://` and `relative://` attachment prefixes. 123 | - [feature:] New `base64://` prefix to attach base64 encoded data streams. 124 | 125 | #### Version 0.7.2 (01.03.2014) 126 | - [enhancement:] Attachments are added with their real name. 127 | 128 | #### Version 0.7.1 (17.12.2013) 129 | - [bugfix:] Only the last attachment was added to the email composer on android. 130 | 131 | #### Version 0.7.0 (05.12.2013) 132 | - Release under the Apache 2.0 license. 133 | - [___change:___] Removed the `callback` property from the `open` interface. 134 | - [___change:___] Renamed the properties `recipients`, `ccRecipients`, `bccRecipients`. 135 | - [bugfix:] Plugin under WP8 throws an error, if recipients were given as arrays. 136 | - [enhancement:] `open` does not block the ui thread on iOS & Android anymore. 137 | 138 | #### Version 0.6.0 (17.11.2013) 139 | - Added WP8 support 140 | - [***deprecated:***] The `callback` property will be removed with v0.7.0. 141 | 142 | #### Version 0.4.2 (17.11.2013) 143 | - [feature:] Added alias `openDraft` to the `open` interface. 144 | 145 | #### Version 0.4.1 (03.11.2013) 146 | - [bugfix]: On Android, the `isServiceAvailable()` interface has returned string values instead of boolean values. 147 | - [bugfix]: Sometimes the device said that no email app is available because of the missing mime type. 148 | 149 | #### Version 0.4.0 (20.08.2013) 150 | - Added Android support
151 | *Based on the EmailComposerWithAttachments Android plugin made by* ***guidosabatini*** 152 | 153 | #### Version 0.2.1 (15.08.2013) 154 | - [bugfix]: Email was not send in HTML format, if the `isHtml` flag was set. 155 | - [bugfix]: `email.open()` without a parameter throw an error. 156 | 157 | #### Version 0.2.0 (13.08.2013) 158 | - Added iOS support
159 | *Based on the EmailComposer(WithAttachments) iOS plugin made by* ***Randy McMillan*** *and* ***guidosabatini*** 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2013-2017 appPlant GmbH 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cordova Email Plugin
[![npm version](https://badge.fury.io/js/cordova-plugin-email-composer.svg)](http://badge.fury.io/js/cordova-plugin-email-composer) [![Code Climate](https://codeclimate.com/github/katzer/cordova-plugin-email-composer/badges/gpa.svg)](https://codeclimate.com/github/katzer/cordova-plugin-email-composer) [![PayPayl donate button](https://img.shields.io/badge/paypal-donate-yellow.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=L3HKQCD9UA35A "Donate once-off to this project using Paypal") 2 | 3 | 4 | 5 | The plugin provides access to the standard interface that manages the editing and sending an email message. You can use this view controller to display a standard email view inside your application and populate the fields of that view with initial values, such as the subject, email recipients, body text, and attachments. The user can edit the initial contents you specify and choose to send the email or cancel the operation. 6 | 7 | Using this interface does not guarantee immediate delivery of the corresponding email message. The user may cancel the creation of the message, and if the user does choose to send the message, the message is only queued in the Mail application outbox. This allows you to generate emails even in situations where the user does not have network access, such as in airplane mode. This interface does not provide a way for you to verify whether emails were actually sent.

8 | 9 | 10 | ## Supported Platforms 11 | 12 | - __Android__ 13 | - __Browser__ 14 | - __iOS__ 15 | - __OSX__ 16 | - __Windows__ 17 | 18 | 19 | ## Installation 20 | 21 | The plugin can be installed via [Cordova-CLI][CLI] and is publicly available on [NPM][npm]. 22 | 23 | Execute from the projects root folder: 24 | 25 | $ cordova plugin add cordova-plugin-email-composer 26 | 27 | Or install a specific version: 28 | 29 | $ cordova plugin add cordova-plugin-email-composer@VERSION 30 | 31 | Or install the latest master version: 32 | 33 | $ cordova plugin add https://github.com/katzer/cordova-plugin-email-composer.git 34 | 35 | Or install from local source: 36 | 37 | $ cordova plugin add --nofetch --nosave 38 | 39 | 40 | ## Usage 41 | 42 | The plugin creates the object `cordova.plugins.email` and is accessible after the *deviceready* event has been fired. 43 | 44 | ```js 45 | document.addEventListener('deviceready', function () { 46 | // cordova.plugins.email is now available 47 | }, false); 48 | ``` 49 | 50 | All properties are optional. After opening the draft the user may have the possibilities to edit the draft from the UI. The callback comes without arguments. 51 | 52 | ```javascript 53 | cordova.plugins.email.open({ 54 | from: String, // sending email account (iOS only) 55 | to: Array, // email addresses for TO field 56 | cc: Array, // email addresses for CC field 57 | bcc: Array, // email addresses for BCC field 58 | attachments: Array, // file paths or base64 data streams 59 | subject: String, // subject of the email 60 | body: String, // email body 61 | isHtml: Boolean // indicats if the body is HTML or plain text (primarily iOS) 62 | }, callback, scope); 63 | ``` 64 | 65 | The following example shows how to create and show an email draft pre-filled with different kind of properties: 66 | 67 | ```javascript 68 | cordova.plugins.email.open({ 69 | to: 'max@mustermann.de', 70 | cc: 'erika@mustermann.de', 71 | bcc: ['john@doe.com', 'jane@doe.com'], 72 | subject: 'Greetings', 73 | body: 'How are you? Nice greetings from Leipzig' 74 | }); 75 | ``` 76 | 77 | Of course its also possible to open a blank draft: 78 | 79 | ```javascript 80 | cordova.plugins.email.open(); 81 | ``` 82 | 83 | Its possible to specify the email client. If the phone isn´t able to handle the specified scheme it will fallback to the system default: 84 | 85 | ```javascript 86 | cordova.plugins.email.open({ app: 'mailto', subject: 'Sent with mailto' }); 87 | ``` 88 | 89 | On Android the app can be specified by either an alias or its package name. The alias _gmail_ is available by default. 90 | 91 | ```javascript 92 | // Add app alias 93 | cordova.plugins.email.addAlias('gmail', 'com.google.android.gm'); 94 | 95 | // Specify app by name or alias 96 | cordova.plugins.email.open({ app: 'gmail', subject: 'Sent from Gmail' }); 97 | ``` 98 | 99 | #### Issues with AndroidX 100 | 101 | If you have issues with AndroidX, simply install the extra plugin 102 | 103 | ``` 104 | cordova plugin add cordova-plugin-androidx-adapter 105 | ``` 106 | 107 | For Ionic/Capacitor you can also try 108 | 109 | ```bash 110 | npm install jetifier 111 | npx jetify 112 | npx cap sync android 113 | ``` 114 | 115 | #### HTML and CSS 116 | 117 | Only the built-in email app for iOS does support HTML and CSS. Some Android clients support rich formatted text. 118 | 119 | Use `isHtml` with caution! It's disabled by default. 120 | 121 | #### Attach Base64 encoded content 122 | The code below shows how to attach an base64 encoded image which will be added as a image with the name *icon.png*. 123 | 124 | ```javascript 125 | cordova.plugins.email.open({ 126 | subject: 'Cordova Icon', 127 | attachments: ['base64:icon.png//iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6...'] 128 | }); 129 | ``` 130 | 131 | #### Attach files from the device storage 132 | The path to the files must be defined absolute from the root of the file system. On Android the user has to allow the app first to read from external storage! 133 | 134 | ```javascript 135 | cordova.plugins.email.open({ 136 | attachments: 'file:///storage/sdcard/icon.png', //=> storage/sdcard/icon.png (Android) 137 | }); 138 | ``` 139 | 140 | #### Attach native app resources 141 | Each app has a resource folder, e.g. the _res_ folder for Android apps or the _Resource_ folder for iOS apps. The following example shows how to attach the app icon from within the app's resource folder. 142 | 143 | ```javascript 144 | cordova.plugins.email.open({ 145 | attachments: 'res://icon.png' //=> res/mipmap/icon (Android) 146 | }); 147 | ``` 148 | 149 | #### Attach assets from the www folder 150 | The path to the files must be defined relative from the root of the mobile web app folder, which is located under the _www_ folder. 151 | 152 | ```javascript 153 | cordova.plugins.email.open({ 154 | attachments: [ 155 | 'file://img/logo.png', //=> assets/www/img/logo.png (Android) 156 | 'file://css/index.css' //=> www/css/index.css (iOS) 157 | ] 158 | }); 159 | ``` 160 | 161 | #### Attach files from the internal app file system 162 | The path must be defined relative from the directory holding application files. 163 | 164 | ```javascript 165 | cordova.plugins.email.open({ 166 | attachments: [ 167 | 'app://databases/db.db3', //=> /data/data//databases/db.db3 (Android) 168 | 'app://databases/db.db3', //=> /Applications//databases/db.db3 (iOS, OSX) 169 | 'app://databases/db.db3', //=> ms-appdata:///databases/db.db3 (Windows) 170 | ] 171 | }); 172 | ``` 173 | 174 | ### Device Configuration 175 | 176 | The email service is only available on devices which have configured an email account. On Android the user has to allow the app first to access account informations. 177 | 178 | ```javascript 179 | cordova.plugins.email.hasAccount(callbackFn); 180 | ``` 181 | 182 | To check for a specific mail client, just pass its uri scheme on iOS, or the package name on Android as first parameter: 183 | 184 | ```javascript 185 | cordova.plugins.email.hasClient('gmail', callbackFn); 186 | ``` 187 | 188 | For Android only, it's possible to get a list of all installed email clients: 189 | 190 | ```javascript 191 | cordova.plugins.email.getClients(function (apps) { 192 | cordova.plugins.email.open({ app: apps[0] }); 193 | }); 194 | ``` 195 | 196 | ## Permissions 197 | 198 | Some functions require permissions on __Android__. The plugin itself does not add them to the manifest nor does it ask for by itself at runtime. 199 | 200 | | Permission | Description | 201 | | ---------- | ----------- | 202 | | `cordova.plugins.email.permission.READ_EXTERNAL_STORAGE` | Is needed to attach external files `file:///` located outside of the app's own file system. | 203 | | `cordova.plugins.email.permission.GET_ACCOUNTS` | Without the permission the `hasAccount()` function wont be able to look for email accounts. | 204 | 205 | To check if a permission has been granted: 206 | 207 | ```javascript 208 | cordova.plugins.email.hasPermission(permission, callbackFn); 209 | ``` 210 | 211 | To request a permission: 212 | 213 | ```javascript 214 | cordova.plugins.email.requestPermission(permission, callbackFn); 215 | ``` 216 | 217 | __Note:__ The author of the app has to make sure that the permission is listed in the manifest. 218 | 219 | 220 | ## Contributing 221 | 222 | 1. Fork it 223 | 2. Create your feature branch (`git checkout -b my-new-feature`) 224 | 3. Commit your changes (`git commit -am 'Add some feature'`) 225 | 4. Push to the branch (`git push origin my-new-feature`) 226 | 5. Create new Pull Request 227 | 228 | 229 | ## License 230 | 231 | This software is released under the [Apache 2.0 License][apache2_license]. 232 | 233 | Made with :yum: from Leipzig 234 | 235 | © 2013 [appPlant GmbH][appplant] 236 | 237 | 238 | [cordova]: https://cordova.apache.org 239 | [CLI]: http://cordova.apache.org/docs/en/edge/guide_cli_index.md.html#The%20Command-line%20Interface 240 | [npm]: https://www.npmjs.com/package/cordova-plugin-email-composer 241 | [email_app]: #specify-email-app 242 | [apache2_license]: http://opensource.org/licenses/Apache-2.0 243 | [appplant]: http://appplant.de 244 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-email-composer", 3 | "version": "0.10.0", 4 | "lockfileVersion": 1 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cordova-plugin-email-composer", 3 | "version": "0.10.1", 4 | "description": "Provides access to the standard interface that manages the editing and sending an email message", 5 | "cordova": { 6 | "id": "cordova-plugin-email-composer", 7 | "platforms": [ 8 | "ios", 9 | "osx", 10 | "android", 11 | "windows", 12 | "browser" 13 | ] 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "git+https://github.com/katzer/cordova-plugin-email-composer.git" 18 | }, 19 | "keywords": [ 20 | "appplant", 21 | "email", 22 | "ecosystem:cordova", 23 | "cordova-ios", 24 | "cordova-osx", 25 | "cordova-android", 26 | "cordova-windows", 27 | "cordova-browser" 28 | ], 29 | "engines": [ 30 | { 31 | "name": "cordova", 32 | "version": ">=6.0.0" 33 | }, 34 | { 35 | "name": "cordova-android", 36 | "version": ">=6.2.0" 37 | }, 38 | { 39 | "name": "cordova-ios", 40 | "version": ">=4.0.0" 41 | }, 42 | { 43 | "name": "cordova-windows", 44 | "version": ">=4.3.0" 45 | } 46 | ], 47 | "author": "Sebastián Katzer", 48 | "license": "Apache-2.0", 49 | "bugs": { 50 | "url": "https://github.com/katzer/cordova-plugin-email-composer/issues" 51 | }, 52 | "homepage": "https://github.com/katzer/cordova-plugin-email-composer#readme", 53 | "dependencies": {} 54 | } 55 | -------------------------------------------------------------------------------- /plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 26 | 27 | EmailComposer 28 | 29 | 30 | Provides access to the standard interface that manages 31 | the editing and sending an email message 32 | 33 | 34 | https://github.com/katzer/cordova-plugin-email-composer.git 35 | 36 | appplant, email 37 | 38 | Apache 2.0 39 | 40 | Sebastián Katzer 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | mailto 65 | googlegmail 66 | ms-outlook 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 126 | 129 | 130 | 131 | 132 | 135 | 136 | 139 | 140 | 143 | 144 | 147 | 148 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /src/android/AssetUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | package de.appplant.cordova.emailcomposer; 21 | 22 | import android.content.Context; 23 | import android.content.res.AssetManager; 24 | import android.content.res.Resources; 25 | import android.net.Uri; 26 | import android.util.Base64; 27 | import android.util.Log; 28 | 29 | import java.io.ByteArrayInputStream; 30 | import java.io.File; 31 | import java.io.FileInputStream; 32 | import java.io.FileOutputStream; 33 | import java.io.InputStream; 34 | 35 | import static de.appplant.cordova.emailcomposer.EmailComposer.LOG_TAG; 36 | 37 | final class AssetUtil { 38 | 39 | // Path where to put tmp the attachments. 40 | private static final String ATTACHMENT_FOLDER = "/email_composer"; 41 | 42 | // Application context 43 | private final Context ctx; 44 | 45 | /** 46 | * Initializes the asset utils. 47 | * 48 | * @param ctx The application context. 49 | */ 50 | AssetUtil (Context ctx) { 51 | this.ctx = ctx; 52 | } 53 | 54 | /** 55 | * Cleans the attachment folder. 56 | * 57 | * @param ctx The application context. 58 | */ 59 | @SuppressWarnings("ResultOfMethodCallIgnored") 60 | static void cleanupAttachmentFolder(Context ctx) { 61 | try { 62 | String path = ctx.getExternalCacheDir() + ATTACHMENT_FOLDER; 63 | File dir = new File(path); 64 | 65 | if (!dir.isDirectory()) 66 | return; 67 | 68 | File[] files = dir.listFiles(); 69 | 70 | for (File file : files) { file.delete(); } 71 | } catch (Exception npe){ 72 | Log.w(LOG_TAG, "Missing external cache dir"); 73 | } 74 | } 75 | 76 | /** 77 | * The URI for an attachment path. 78 | * 79 | * @param path The given path to the attachment. 80 | * @return The URI pointing to the given path. 81 | */ 82 | Uri parse (String path) { 83 | Uri uri; 84 | 85 | if (path.startsWith("res:")) { 86 | uri = getUriForResourcePath(path); 87 | } else if (path.startsWith("app://")) { 88 | uri = getUriForAppInternalPath(path); 89 | } else if (path.startsWith("file:///")) { 90 | uri = getUriForAbsolutePath(path); 91 | } else if (path.startsWith("file://")) { 92 | uri = getUriForAssetPath(path); 93 | } else if (path.startsWith("base64:")) { 94 | uri = getUriForBase64Content(path); 95 | } else { 96 | uri = Uri.parse(path); 97 | } 98 | 99 | return uri; 100 | } 101 | 102 | /** 103 | * The URI for a file. 104 | * 105 | * @param path The given absolute path. 106 | * @return The URI pointing to the given path. 107 | */ 108 | private Uri getUriForAbsolutePath (String path) { 109 | String absPath = path.replaceFirst("file://", ""); 110 | File file = new File(absPath); 111 | 112 | if (!file.exists()) { 113 | Log.e(LOG_TAG, "File not found: " + absPath); 114 | } 115 | 116 | return getUriForFile(ctx, file); 117 | } 118 | 119 | /** 120 | * The URI for an asset. 121 | * 122 | * @param path The given asset path. 123 | * @return The URI pointing to the given path. 124 | */ 125 | @SuppressWarnings("ResultOfMethodCallIgnored") 126 | private Uri getUriForAssetPath (String path) { 127 | String resPath = path.replaceFirst("file:/", "www"); 128 | String fileName = resPath.substring(resPath.lastIndexOf('/') + 1); 129 | File dir = ctx.getExternalCacheDir(); 130 | 131 | if (dir == null) { 132 | Log.e(LOG_TAG, "Missing external cache dir"); 133 | return Uri.EMPTY; 134 | } 135 | 136 | String storage = dir.toString() + ATTACHMENT_FOLDER; 137 | File file = new File(storage, fileName); 138 | new File(storage).mkdir(); 139 | 140 | try { 141 | AssetManager assets = ctx.getAssets(); 142 | InputStream in = assets.open(resPath); 143 | FileOutputStream out = new FileOutputStream(file); 144 | copyStream(in, out); 145 | } catch (Exception e) { 146 | Log.e(LOG_TAG, "File not found: " + resPath); 147 | e.printStackTrace(); 148 | } 149 | 150 | return getUriForFile(ctx, file); 151 | } 152 | 153 | /** 154 | * The URI for an internal file. 155 | * 156 | * @param path The given asset path. 157 | * @return The URI pointing to the given path. 158 | */ 159 | @SuppressWarnings("ResultOfMethodCallIgnored") 160 | private Uri getUriForAppInternalPath (String path) { 161 | String resPath = path.replaceFirst("app:/", ""); 162 | String fileName = resPath.substring(resPath.lastIndexOf('/') + 1); 163 | File dir = ctx.getExternalCacheDir(); 164 | 165 | if (dir == null) { 166 | Log.e("EmailComposer", "Missing external cache dir"); 167 | return Uri.EMPTY; 168 | } 169 | 170 | String storage = dir.toString() + ATTACHMENT_FOLDER; 171 | File file = new File(storage, fileName); 172 | new File(storage).mkdir(); 173 | 174 | File filesDir = ctx.getFilesDir(); 175 | String absPath = filesDir.getAbsolutePath() + "/.." + resPath; 176 | 177 | try { 178 | InputStream in = new FileInputStream(absPath); 179 | FileOutputStream out = new FileOutputStream(file); 180 | copyStream(in, out); 181 | } catch (Exception e) { 182 | Log.e(LOG_TAG, "File not found: " + absPath); 183 | e.printStackTrace(); 184 | } 185 | 186 | return getUriForFile(ctx, file); 187 | } 188 | 189 | /** 190 | * The URI for a resource. 191 | * 192 | * @param path The given relative path. 193 | * @return The URI pointing to the given path 194 | */ 195 | @SuppressWarnings("ResultOfMethodCallIgnored") 196 | private Uri getUriForResourcePath (String path) { 197 | String resPath = path.replaceFirst("res://", ""); 198 | String fileName = resPath.substring(resPath.lastIndexOf('/') + 1); 199 | String resName = fileName.substring(0, fileName.lastIndexOf('.')); 200 | String extension = resPath.substring(resPath.lastIndexOf('.')); 201 | File dir = ctx.getExternalCacheDir(); 202 | int resId = getResId(resPath); 203 | 204 | if (dir == null) { 205 | Log.e(LOG_TAG, "Missing external cache dir"); 206 | return Uri.EMPTY; 207 | } 208 | 209 | if (resId == 0) { 210 | Log.e(LOG_TAG, "File not found: " + resPath); 211 | } 212 | 213 | String storage = dir.toString() + ATTACHMENT_FOLDER; 214 | File file = new File(storage, resName + extension); 215 | new File(storage).mkdir(); 216 | 217 | try { 218 | Resources res = ctx.getResources(); 219 | InputStream in = res.openRawResource(resId); 220 | FileOutputStream out = new FileOutputStream(file); 221 | copyStream(in, out); 222 | } catch (Exception e) { 223 | Log.e(LOG_TAG, "File not found: " + resPath); 224 | e.printStackTrace(); 225 | } 226 | 227 | return getUriForFile(ctx, file); 228 | } 229 | 230 | /** 231 | * The URI for a base64 encoded content. 232 | * 233 | * @param str The given base64 encoded content. 234 | * @return The URI including the given content. 235 | */ 236 | @SuppressWarnings("ResultOfMethodCallIgnored") 237 | private Uri getUriForBase64Content (String str) { 238 | String resName = str.substring(str.indexOf(":") + 1, str.indexOf("//")); 239 | String resData = str.substring(str.indexOf("//") + 2); 240 | File dir = ctx.getExternalCacheDir(); 241 | 242 | if (dir == null) { 243 | Log.e(LOG_TAG, "Missing external cache dir"); 244 | return Uri.EMPTY; 245 | } 246 | 247 | String storage = dir.toString() + ATTACHMENT_FOLDER; 248 | File file = new File(storage, resName); 249 | new File(storage).mkdir(); 250 | 251 | try { 252 | byte[] bytes = Base64.decode(resData, 0); 253 | InputStream in = new ByteArrayInputStream(bytes); 254 | FileOutputStream out = new FileOutputStream(file); 255 | copyStream(in, out); 256 | } catch (Exception e) { 257 | Log.e(LOG_TAG, "Invalid Base64 string"); 258 | e.printStackTrace(); 259 | } 260 | 261 | return getUriForFile(ctx, file); 262 | } 263 | 264 | /** 265 | * Get content URI for the specified file. 266 | * 267 | * @param ctx The application context. 268 | * @param file The file to get the URI. 269 | * 270 | * @return content://... 271 | */ 272 | private Uri getUriForFile(Context ctx, File file) { 273 | String authority = ctx.getPackageName() + ".emailcomposer.provider"; 274 | 275 | try { 276 | return Provider.getUriForFile(ctx, authority, file); 277 | } catch (Exception e) { 278 | Log.e(LOG_TAG, "Failed to get uri for file"); 279 | e.printStackTrace(); 280 | return Uri.EMPTY; 281 | } 282 | } 283 | 284 | /** 285 | * Writes an InputStream to an OutputStream 286 | * 287 | * @param in The input stream. 288 | * @param out The output stream. 289 | */ 290 | private void copyStream (InputStream in, FileOutputStream out) { 291 | byte[] buffer = new byte[1024]; 292 | int read; 293 | 294 | try { 295 | while ((read = in.read(buffer)) != -1) { 296 | out.write(buffer, 0, read); 297 | } 298 | out.flush(); 299 | out.close(); 300 | } catch (Exception e) { 301 | e.printStackTrace(); 302 | } 303 | } 304 | 305 | /** 306 | * Returns the resource ID for the given resource path. 307 | * 308 | * @return The resource ID for the given resource. 309 | */ 310 | private int getResId (String resPath) { 311 | Resources res = ctx.getResources(); 312 | String pkgName = ctx.getPackageName(); 313 | String dirName = "drawable"; 314 | String fileName = resPath; 315 | 316 | if (resPath.contains("/")) { 317 | dirName = resPath.substring(0, resPath.lastIndexOf('/')); 318 | fileName = resPath.substring(resPath.lastIndexOf('/') + 1); 319 | } 320 | 321 | String resName = fileName.substring(0, fileName.lastIndexOf('.')); 322 | int resId = res.getIdentifier(resName, dirName, pkgName); 323 | 324 | if (resId == 0) { 325 | resId = res.getIdentifier(resName, "mipmap", pkgName); 326 | } 327 | 328 | if (resId == 0) { 329 | resId = res.getIdentifier(resName, "drawable", pkgName); 330 | } 331 | 332 | return resId; 333 | } 334 | 335 | } 336 | -------------------------------------------------------------------------------- /src/android/EmailComposer.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | package de.appplant.cordova.emailcomposer; 21 | 22 | import android.content.ActivityNotFoundException; 23 | import android.content.Context; 24 | import android.content.Intent; 25 | 26 | import org.apache.cordova.CallbackContext; 27 | import org.apache.cordova.CordovaInterface; 28 | import org.apache.cordova.CordovaPlugin; 29 | import org.apache.cordova.CordovaWebView; 30 | import org.apache.cordova.PluginResult; 31 | import org.apache.cordova.PluginResult.Status; 32 | import org.json.JSONArray; 33 | import org.json.JSONException; 34 | import org.json.JSONObject; 35 | 36 | import java.util.ArrayList; 37 | import java.util.List; 38 | 39 | import static android.Manifest.permission.GET_ACCOUNTS; 40 | import static android.Manifest.permission.READ_EXTERNAL_STORAGE; 41 | import static android.content.pm.PackageManager.PERMISSION_GRANTED; 42 | 43 | @SuppressWarnings({"Convert2Diamond", "Convert2Lambda"}) 44 | public class EmailComposer extends CordovaPlugin { 45 | 46 | // The log tag for this plugin 47 | static final String LOG_TAG = "EmailComposer"; 48 | 49 | // The callback context used when calling back into JavaScript 50 | private CallbackContext command; 51 | 52 | /** 53 | * Delete externalCacheDirectory on app start 54 | * 55 | * @param cordova Cordova-instance 56 | * @param webView CordovaWebView-instance 57 | */ 58 | @Override 59 | public void initialize(CordovaInterface cordova, CordovaWebView webView) { 60 | super.initialize(cordova, webView); 61 | AssetUtil.cleanupAttachmentFolder(getContext()); 62 | } 63 | 64 | /** 65 | * Executes the request. 66 | *

67 | * This method is called from the WebView thread. 68 | * To do a non-trivial amount of work, use: 69 | * cordova.getThreadPool().execute(runnable); 70 | *

71 | * To run on the UI thread, use: 72 | * cordova.getActivity().runOnUiThread(runnable); 73 | * 74 | * @param action The action to execute. 75 | * @param args The exec() arguments in JSON form. 76 | * @param callback The callback context used when calling 77 | * back into JavaScript. 78 | * @return Whether the action was valid. 79 | */ 80 | @Override 81 | public boolean execute(String action, JSONArray args, 82 | CallbackContext callback) throws JSONException { 83 | 84 | this.command = callback; 85 | 86 | if ("open".equalsIgnoreCase(action)) { 87 | open(args.getJSONObject(0)); 88 | } else if ("client".equalsIgnoreCase(action)) { 89 | client(args.getString(0)); 90 | } else if ("check".equalsIgnoreCase(action)) { 91 | check(args.optInt(0, 0)); 92 | } else if ("request".equalsIgnoreCase(action)) { 93 | request(args.optInt(0, 0)); 94 | } else if ("clients".equalsIgnoreCase(action)) { 95 | clients(); 96 | } else if ("account".equalsIgnoreCase(action)) { 97 | account(); 98 | } else { 99 | return false; 100 | } 101 | 102 | return true; 103 | } 104 | 105 | /** 106 | * Returns the application context. 107 | */ 108 | private Context getContext() { 109 | return cordova.getActivity(); 110 | } 111 | 112 | /** 113 | * Finds out if the given mail client is installed. 114 | * 115 | * @param id The app id. 116 | */ 117 | private void client(final String id) { 118 | cordova.getThreadPool().execute(new Runnable() { 119 | public void run() { 120 | Impl impl = new Impl(getContext()); 121 | boolean res = impl.isAppInstalled(id); 122 | 123 | sendResult(new PluginResult(Status.OK, res)); 124 | } 125 | }); 126 | } 127 | 128 | /** 129 | * List of the package IDs from all available email clients. 130 | */ 131 | private void clients() { 132 | cordova.getThreadPool().execute(new Runnable() { 133 | public void run() { 134 | Impl impl = new Impl(getContext()); 135 | List ids = impl.getEmailClientIds(); 136 | List res = new ArrayList(); 137 | 138 | for (String id:ids) { 139 | res.add(new PluginResult(Status.OK, id)); 140 | } 141 | 142 | sendResult(new PluginResult(Status.OK, res)); 143 | } 144 | }); 145 | } 146 | 147 | /** 148 | * Tries to figure out if an email account is setup. 149 | */ 150 | private void account() { 151 | cordova.getThreadPool().execute(new Runnable() { 152 | public void run() { 153 | Impl impl = new Impl(getContext()); 154 | boolean res = impl.isEmailAccountConfigured(); 155 | 156 | sendResult(new PluginResult(Status.OK, res)); 157 | } 158 | }); 159 | } 160 | 161 | /** 162 | * Sends an intent to the email app. 163 | * 164 | * @param props The email properties like subject or body 165 | */ 166 | private void open(final JSONObject props) { 167 | final EmailComposer me = this; 168 | 169 | cordova.getThreadPool().execute(new Runnable() { 170 | public void run() { 171 | try { 172 | Impl impl = new Impl(getContext()); 173 | Intent draft = impl.getDraft(props); 174 | 175 | cordova.startActivityForResult(me, draft, 0); 176 | } catch (ActivityNotFoundException e) { 177 | onActivityResult(0, 0, null); 178 | } 179 | } 180 | }); 181 | } 182 | 183 | /** 184 | * Check if the given permissions has been granted. 185 | * 186 | * @param code The code number of the permission to check for. 187 | */ 188 | private void check(int code) { 189 | check(getPermission(code)); 190 | } 191 | 192 | /** 193 | * Check if the given permission has been granted. 194 | * 195 | * @param permission The permission to check for. 196 | */ 197 | private void check(String permission) { 198 | Boolean granted = cordova.hasPermission(permission); 199 | sendResult(new PluginResult(Status.OK, granted)); 200 | } 201 | 202 | /** 203 | * Request given permission. 204 | * 205 | * @param code The code number of the permission to request for. 206 | */ 207 | private void request(int code) { 208 | cordova.requestPermission(this, code, getPermission(code)); 209 | } 210 | 211 | /** 212 | * Returns the corresponding permission for the internal code. 213 | * 214 | * @param code The internal code number. 215 | * 216 | * @return The Android permission string or "". 217 | */ 218 | private String getPermission(int code) { 219 | switch (code) { 220 | case 1: return READ_EXTERNAL_STORAGE; 221 | case 2: return GET_ACCOUNTS; 222 | default: return ""; 223 | } 224 | } 225 | 226 | /** 227 | * Send plugin result and reset plugin state. 228 | * 229 | * @param result The result to send to the webview. 230 | */ 231 | private void sendResult(PluginResult result) { 232 | if (command != null) { 233 | command.sendPluginResult(result); 234 | } 235 | 236 | command = null; 237 | } 238 | 239 | /** 240 | * Called when an activity you launched exits, giving you the reqCode you 241 | * started it with, the resCode it returned, and any additional data from it. 242 | * 243 | * @param reqCode The request code originally supplied to startActivityForResult(), 244 | * allowing you to identify who this result came from. 245 | * @param resCode The integer result code returned by the child activity 246 | * through its setResult(). 247 | * @param intent An Intent, which can return result data to the caller 248 | * (various data can be attached to Intent "extras"). 249 | */ 250 | @Override 251 | public void onActivityResult(int reqCode, int resCode, Intent intent) { 252 | sendResult(new PluginResult(Status.OK)); 253 | } 254 | 255 | /** 256 | * Called by the system when the user grants permissions. 257 | * 258 | * @param code The requested code. 259 | * @param permissions The requested permissions. 260 | * @param grantResults The grant result for the requested permissions. 261 | */ 262 | @Override 263 | public void onRequestPermissionResult(int code, String[] permissions, 264 | int[] grantResults) { 265 | 266 | List messages = new ArrayList(); 267 | Boolean granted = false; 268 | 269 | if (grantResults.length > 0) { 270 | granted = grantResults[0] == PERMISSION_GRANTED; 271 | } 272 | 273 | messages.add(new PluginResult(Status.OK, granted)); 274 | messages.add(new PluginResult(Status.OK, code)); 275 | 276 | sendResult(new PluginResult(Status.OK, messages)); 277 | } 278 | 279 | } 280 | -------------------------------------------------------------------------------- /src/android/Impl.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | package de.appplant.cordova.emailcomposer; 21 | 22 | import android.accounts.Account; 23 | import android.accounts.AccountManager; 24 | import android.annotation.SuppressLint; 25 | import android.content.Context; 26 | import android.content.Intent; 27 | import android.content.pm.ActivityInfo; 28 | import android.content.pm.PackageManager; 29 | import android.content.pm.ResolveInfo; 30 | import android.net.Uri; 31 | import android.os.Parcelable; 32 | import android.text.Html; 33 | import android.util.Log; 34 | import android.util.Patterns; 35 | 36 | import org.json.JSONArray; 37 | import org.json.JSONObject; 38 | 39 | import java.util.ArrayList; 40 | import java.util.List; 41 | import java.util.regex.Pattern; 42 | 43 | import static android.content.Intent.ACTION_SENDTO; 44 | import static android.content.Intent.EXTRA_INITIAL_INTENTS; 45 | import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; 46 | import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; 47 | import static android.content.Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP; 48 | import static de.appplant.cordova.emailcomposer.EmailComposer.LOG_TAG; 49 | 50 | @SuppressWarnings("Convert2Diamond") 51 | class Impl { 52 | 53 | // The default mailto: scheme. 54 | private static final String MAILTO_SCHEME = "mailto:"; 55 | 56 | // The application context. 57 | private final Context ctx; 58 | 59 | /** 60 | * Initializes the class. 61 | * 62 | * @param ctx The application context. 63 | */ 64 | Impl (Context ctx) { 65 | this.ctx = ctx; 66 | } 67 | 68 | /** 69 | * The intent with the containing email properties. 70 | * 71 | * @param params The email properties like subject or body. 72 | * @return The resulting intent. 73 | */ 74 | Intent getDraft (JSONObject params) { 75 | Intent draft = getFilledEmailIntent(params); 76 | String app = params.optString("app", MAILTO_SCHEME); 77 | String header = params.optString("chooserHeader", "Open with"); 78 | 79 | if (!app.equals(MAILTO_SCHEME) && isAppInstalled(app)) { 80 | return draft.setPackage(app); 81 | } 82 | 83 | List targets = new ArrayList<>(); 84 | 85 | for (String clientId : getEmailClientIds()) { 86 | Intent target = (Intent) draft.clone(); 87 | targets.add(target.setPackage(clientId)); 88 | } 89 | 90 | return Intent.createChooser(draft, header) 91 | .putExtra(EXTRA_INITIAL_INTENTS, targets.toArray(new Parcelable[0])); 92 | } 93 | 94 | /** 95 | * The intent with the containing email properties. 96 | * 97 | * @param params The email properties like subject or body. 98 | * @return The resulting intent. 99 | */ 100 | private Intent getFilledEmailIntent (JSONObject params) { 101 | Intent draft = getEmailIntent(); 102 | 103 | if (params.has("subject")) 104 | setSubject(params, draft); 105 | 106 | if (params.has("body")) 107 | setBody(params, draft); 108 | 109 | if (params.has("to")) 110 | setRecipients(params, draft); 111 | 112 | if (params.has("cc")) 113 | setCcRecipients(params, draft); 114 | 115 | if (params.has("bcc")) 116 | setBccRecipients(params, draft); 117 | 118 | if (params.has("attachments")) 119 | setAttachments(params, draft); 120 | 121 | return draft; 122 | } 123 | 124 | /** 125 | * Setter for the subject. 126 | * 127 | * @param params The email properties like subject or body. 128 | * @param draft The intent to send. 129 | */ 130 | private void setSubject (JSONObject params, Intent draft) { 131 | String subject = params.optString("subject"); 132 | draft.putExtra(Intent.EXTRA_SUBJECT, subject); 133 | } 134 | 135 | /** 136 | * Setter for the body. 137 | * 138 | * @param params The email properties like subject or body. 139 | * @param draft The intent to send. 140 | */ 141 | private void setBody (JSONObject params, Intent draft) { 142 | String body = fixLineBreaks(params.optString("body")); 143 | boolean isHTML = params.optBoolean("isHtml"); 144 | CharSequence text = isHTML ? Html.fromHtml(body) : body; 145 | 146 | draft.putExtra(Intent.EXTRA_TEXT, text); 147 | } 148 | 149 | /** 150 | * Setter for the recipients. 151 | * 152 | * @param params The email properties like subject or body. 153 | * @param draft The intent to send. 154 | */ 155 | private void setRecipients (JSONObject params, Intent draft) { 156 | insertRecipients(draft, params, "to", Intent.EXTRA_EMAIL); 157 | } 158 | 159 | /** 160 | * Setter for the cc recipients. 161 | * 162 | * @param params The email properties like subject or body. 163 | * @param draft The intent to send. 164 | */ 165 | private void setCcRecipients (JSONObject params, Intent draft) { 166 | insertRecipients(draft, params, "cc", Intent.EXTRA_CC); 167 | } 168 | 169 | /** 170 | * Setter for the bcc recipients. 171 | * 172 | * @param params The email properties like subject or body. 173 | * @param draft The intent to send. 174 | */ 175 | private void setBccRecipients (JSONObject params, Intent draft) { 176 | insertRecipients(draft, params, "bcc", Intent.EXTRA_BCC); 177 | } 178 | 179 | /** 180 | * Insert the recipients into the email draft intent. 181 | * 182 | * @param draft The intent to send. 183 | * @param params The email properties like subject or body. 184 | * @param key The key where to find the recipients. 185 | * @param extra The key where to insert the recipients. 186 | */ 187 | private void insertRecipients (Intent draft, JSONObject params, 188 | String key, String extra) { 189 | 190 | JSONArray recipients = params.optJSONArray(key); 191 | String[] receivers = new String[recipients.length()]; 192 | 193 | for (int i = 0; i < recipients.length(); i++) { 194 | receivers[i] = recipients.optString(i); 195 | } 196 | 197 | draft.putExtra(extra, receivers); 198 | } 199 | 200 | /** 201 | * Setter for the attachments. 202 | * 203 | * @param params The email properties like subject or body. 204 | * @param draft The intent to send. 205 | */ 206 | private void setAttachments (JSONObject params, Intent draft) { 207 | JSONArray attachments = params.optJSONArray("attachments"); 208 | ArrayList uris = new ArrayList(); 209 | AssetUtil assets = new AssetUtil(ctx); 210 | 211 | for (int i = 0; i < attachments.length(); i++) { 212 | Uri uri = assets.parse(attachments.optString(i)); 213 | if (uri != null && uri != Uri.EMPTY) uris.add(uri); 214 | } 215 | 216 | if (uris.isEmpty()) 217 | return; 218 | 219 | draft.setAction(Intent.ACTION_SEND_MULTIPLE) 220 | .setType("*/*") 221 | .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) 222 | .putExtra(Intent.EXTRA_STREAM, uris); 223 | 224 | if (uris.size() > 1) 225 | return; 226 | 227 | draft.setAction(Intent.ACTION_SEND) 228 | .putExtra(Intent.EXTRA_STREAM, uris.get(0)); 229 | } 230 | 231 | /** 232 | * If email apps are available. 233 | * 234 | * @return true if available, otherwise false 235 | */ 236 | @SuppressLint("MissingPermission") 237 | boolean isEmailAccountConfigured() { 238 | AccountManager am = AccountManager.get(ctx); 239 | 240 | try { 241 | Pattern emailPattern = Patterns.EMAIL_ADDRESS; 242 | 243 | for (Account account : am.getAccounts()) { 244 | if (emailPattern.matcher(account.name).matches()) { 245 | return true; 246 | } 247 | } 248 | } catch (Exception e) { 249 | Log.w(LOG_TAG, "Missing GET_ACCOUNTS permission."); 250 | } 251 | 252 | return false; 253 | } 254 | 255 | /** 256 | * Get the info for all available email client activities. 257 | */ 258 | private List getEmailClients() { 259 | Intent intent = getEmailIntent(); 260 | PackageManager pm = ctx.getPackageManager(); 261 | List apps = pm.queryIntentActivities(intent, 0); 262 | List list = new ArrayList<>(); 263 | 264 | for (ResolveInfo app : apps) { 265 | if (app.activityInfo.isEnabled()) { 266 | list.add(app.activityInfo); 267 | } 268 | } 269 | 270 | return list; 271 | } 272 | 273 | /** 274 | * Get package IDs for all available email clients. 275 | */ 276 | List getEmailClientIds() { 277 | List ids = new ArrayList<>(); 278 | 279 | for (ActivityInfo app : getEmailClients()) { 280 | ids.add(app.packageName); 281 | } 282 | 283 | return ids; 284 | } 285 | 286 | /** 287 | * Ask the package manager if the app is installed on the device. 288 | * 289 | * @param id The app id. 290 | * 291 | * @return true if yes otherwise false. 292 | */ 293 | boolean isAppInstalled (String id) { 294 | 295 | if (id.equalsIgnoreCase(MAILTO_SCHEME)) { 296 | Intent intent = getEmailIntent(); 297 | PackageManager pm = ctx.getPackageManager(); 298 | int apps = pm.queryIntentActivities(intent, 0).size(); 299 | 300 | return (apps > 0); 301 | } 302 | 303 | try { 304 | return ctx.getPackageManager() 305 | .getPackageInfo(id, 0) 306 | .applicationInfo.enabled; 307 | } catch (PackageManager.NameNotFoundException e) { 308 | return false; 309 | } 310 | } 311 | 312 | /** 313 | * Setup an intent to send to email apps only. 314 | * 315 | * @return intent 316 | */ 317 | private static Intent getEmailIntent() { 318 | Intent intent = new Intent(ACTION_SENDTO, Uri.parse(MAILTO_SCHEME)); 319 | 320 | intent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); 321 | intent.addFlags(FLAG_ACTIVITY_PREVIOUS_IS_TOP); 322 | 323 | return intent; 324 | } 325 | 326 | /** 327 | * Fix line breaks within the provided text. 328 | * 329 | * @param text The text where to fix the line breaks. 330 | * 331 | * @return The fixed text. 332 | */ 333 | private static String fixLineBreaks (String text) { 334 | return text.replaceAll("\r\n", "\n"); 335 | } 336 | 337 | } 338 | -------------------------------------------------------------------------------- /src/android/Provider.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | package de.appplant.cordova.emailcomposer; 21 | 22 | import androidx.core.content.FileProvider; 23 | 24 | public class Provider extends FileProvider { 25 | // Nothing to do here 26 | } -------------------------------------------------------------------------------- /src/android/xml/emailcomposer_provider_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/browser/EmailComposerProxy.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | /** 21 | * Tries to find out if the device has an configured email account. 22 | * 23 | * @param [ Function ] success Success callback 24 | * @param [ Function ] error Error callback 25 | * @param [ Array ] args Interface arguments 26 | * 27 | * @return [ Void ] 28 | */ 29 | exports.account = function (success, error, args) { 30 | success(null); 31 | }; 32 | 33 | /** 34 | * Tries to find out if the device has an installed email client. 35 | * 36 | * @param [ Function ] success Success callback 37 | * @param [ Function ] error Error callback 38 | * @param [ Array ] args Interface arguments 39 | * 40 | * @return [ Void ] 41 | */ 42 | exports.client = function (success, error, args) { 43 | success(null); 44 | }; 45 | 46 | /** 47 | * Displays the email composer pre-filled with data. 48 | * 49 | * @param [ Function ] success Success callback 50 | * @param [ Function ] error Error callback 51 | * @param [ Array ] args Interface arguments 52 | * 53 | * @return [ Void ] 54 | */ 55 | exports.open = function (success, error, args) { 56 | var props = args[0], 57 | mailto = 'mailto:' + exports.encodeURIs(props.to), 58 | options = ''; 59 | 60 | if (props.subject !== '') { 61 | options += '&subject=' + encodeURIComponent(props.subject); 62 | } 63 | 64 | if (props.body !== '') { 65 | options += '&body=' + encodeURIComponent(props.body); 66 | } 67 | 68 | if (props.cc.length > 0) { 69 | options += '&cc=' + exports.encodeURIs(props.cc); 70 | } 71 | 72 | if (props.bcc.length > 0) { 73 | options += '&bcc=' + exports.encodeURIs(props.bcc); 74 | } 75 | 76 | if (options !== '') { 77 | mailto += '?' + options.substring(1); 78 | } 79 | 80 | window.location.href = mailto; 81 | 82 | success(); 83 | }; 84 | 85 | /** 86 | * Convert list of uris to an encoded string. 87 | * 88 | * @param [ Array ] uris List of uris to encode. 89 | * 90 | * @return [ String ] 91 | */ 92 | exports.encodeURIs = function (uris) { 93 | return encodeURIComponent(uris.join(';')); 94 | }; 95 | 96 | require('cordova/exec/proxy').add('EmailComposer', exports); 97 | -------------------------------------------------------------------------------- /src/ios/APPEmailComposer.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import 21 | #import 22 | #import 23 | 24 | @interface APPEmailComposer : CDVPlugin 25 | 26 | // Shows the email composer view with pre-filled data 27 | - (void) open:(CDVInvokedUrlCommand*)command; 28 | // Checks if an email account is configured 29 | - (void) account:(CDVInvokedUrlCommand*)command; 30 | // Checks if the specified email client is installed 31 | - (void) client:(CDVInvokedUrlCommand*)command; 32 | 33 | @end 34 | -------------------------------------------------------------------------------- /src/ios/APPEmailComposer.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import "APPEmailComposer.h" 21 | #import "APPEmailComposerImpl.h" 22 | 23 | @interface APPEmailComposer () 24 | 25 | // Reference is needed because of the async delegate 26 | @property (nonatomic, strong) CDVInvokedUrlCommand* command; 27 | // Implements the core functionality 28 | @property (nonatomic, strong) APPEmailComposerImpl* impl; 29 | 30 | @end 31 | 32 | @implementation APPEmailComposer 33 | 34 | @synthesize command, impl; 35 | 36 | #pragma mark - 37 | #pragma mark Lifecycle 38 | 39 | /** 40 | * Initialize the core impl object which does the main stuff. 41 | */ 42 | - (void) pluginInitialize 43 | { 44 | self.impl = [[APPEmailComposerImpl alloc] init]; 45 | } 46 | 47 | #pragma mark - 48 | #pragma mark Public 49 | 50 | /** 51 | * Checks if an email account is configured. 52 | */ 53 | - (void) account:(CDVInvokedUrlCommand*)cmd 54 | { 55 | [self.commandDelegate runInBackground:^{ 56 | bool res = [self.impl canSendMail]; 57 | CDVPluginResult* result; 58 | 59 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK 60 | messageAsBool:res]; 61 | 62 | [self.commandDelegate sendPluginResult:result 63 | callbackId:cmd.callbackId]; 64 | }]; 65 | } 66 | 67 | /** 68 | * Checks if an email client is available which responds to the scheme. 69 | */ 70 | - (void) client:(CDVInvokedUrlCommand*)cmd 71 | { 72 | [self.commandDelegate runInBackground:^{ 73 | NSString* scheme = [cmd argumentAtIndex:0]; 74 | bool res = [self.impl canOpenScheme:scheme]; 75 | CDVPluginResult* result; 76 | 77 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK 78 | messageAsBool:res]; 79 | 80 | [self.commandDelegate sendPluginResult:result 81 | callbackId:cmd.callbackId]; 82 | }]; 83 | } 84 | 85 | /** 86 | * Show the email composer view with pre-filled data. 87 | */ 88 | - (void) open:(CDVInvokedUrlCommand*)cmd 89 | { 90 | NSDictionary* props = cmd.arguments[0]; 91 | 92 | self.command = cmd; 93 | 94 | [self.commandDelegate runInBackground:^{ 95 | NSString* scheme = [props objectForKey:@"app"]; 96 | 97 | if ([self canUseAppleMail:scheme]) { 98 | [self presentMailComposerFromProperties:props]; 99 | } else { 100 | [self openURLFromProperties:props]; 101 | } 102 | }]; 103 | } 104 | 105 | #pragma mark - 106 | #pragma mark MFMailComposeViewControllerDelegate 107 | 108 | /** 109 | * Delegate will be called after the mail composer did finish an action 110 | * to dismiss the view. 111 | */ 112 | - (void) mailComposeController:(MFMailComposeViewController*)controller 113 | didFinishWithResult:(MFMailComposeResult)result 114 | error:(NSError*)error 115 | { 116 | [controller dismissViewControllerAnimated:YES completion:NULL]; 117 | 118 | switch(result) { 119 | case MFMailComposeResultSent: 120 | [self execCallback:YES]; 121 | case MFMailComposeResultSaved: 122 | [self execCallback:YES]; 123 | default: 124 | [self execCallback:NO]; 125 | } 126 | } 127 | 128 | #pragma mark - 129 | #pragma mark Private 130 | 131 | /** 132 | * Displays the email draft. 133 | */ 134 | - (void) presentMailComposerFromProperties:(NSDictionary*)props 135 | { 136 | dispatch_async(dispatch_get_main_queue(), ^{ 137 | MFMailComposeViewController* draft = 138 | [self.impl mailComposerFromProperties:props delegateTo:self]; 139 | 140 | if (!draft) { 141 | [self execCallback]; 142 | return; 143 | } 144 | 145 | [self.viewController presentViewController:draft 146 | animated:YES 147 | completion:NULL]; 148 | }); 149 | 150 | } 151 | 152 | /** 153 | * Instructs the application to open the specified URL. 154 | */ 155 | - (void) openURLFromProperties:(NSDictionary*)props 156 | { 157 | NSURL* url = [self.impl urlFromProperties:props]; 158 | 159 | dispatch_async(dispatch_get_main_queue(), ^{ 160 | [[UIApplication sharedApplication] openURL:url 161 | options:@{} 162 | completionHandler:^(BOOL success) { 163 | [self execCallback: success]; 164 | }]; 165 | }); 166 | } 167 | 168 | /** 169 | * If the specified app if the buil-in iMail framework can be used. 170 | */ 171 | - (BOOL) canUseAppleMail:(NSString*) scheme 172 | { 173 | return [scheme hasPrefix:@"mailto"]; 174 | } 175 | 176 | /** 177 | * Invokes the callback without any parameter. 178 | */ 179 | - (void) execCallback { 180 | [self execCallback:NO]; 181 | } 182 | 183 | - (void) execCallback:(BOOL)success 184 | { 185 | 186 | CDVPluginResult *result = [CDVPluginResult 187 | resultWithStatus:CDVCommandStatus_OK messageAsBool:success]; 188 | 189 | [self.commandDelegate sendPluginResult:result 190 | callbackId:self.command.callbackId]; 191 | } 192 | 193 | @end 194 | -------------------------------------------------------------------------------- /src/ios/APPEmailComposerImpl.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import 21 | 22 | @interface APPEmailComposerImpl : NSObject 23 | 24 | // Checks if the mail composer is able to send mails 25 | - (bool) canSendMail; 26 | // Checks if the mail composer is able to open the specified mail client 27 | - (bool) canOpenScheme:(NSString*)scheme; 28 | // Creates an mailto-url-sheme 29 | - (NSURL*) urlFromProperties:(NSDictionary*)props; 30 | // Instantiates an email composer view 31 | - (MFMailComposeViewController*) mailComposerFromProperties:(NSDictionary*)props 32 | delegateTo:(id)receiver; 33 | 34 | @end 35 | -------------------------------------------------------------------------------- /src/ios/APPEmailComposerImpl.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import "APPEmailComposerImpl.h" 21 | #import 22 | #import 23 | #import 24 | 25 | @implementation APPEmailComposerImpl 26 | 27 | #pragma mark - 28 | #pragma mark Public 29 | 30 | /** 31 | * Checks if the mail composer is able to send mails. 32 | */ 33 | - (bool) canSendMail 34 | { 35 | return [MFMailComposeViewController canSendMail]; 36 | } 37 | 38 | /** 39 | * Checks if an app is available to handle the specified scheme. 40 | * 41 | * @param scheme An URL scheme, that defaults to 'mailto: 42 | */ 43 | - (bool) canOpenScheme:(NSString *)scheme 44 | { 45 | __block bool canOpen = false; 46 | NSCharacterSet *set = [NSCharacterSet URLFragmentAllowedCharacterSet]; 47 | 48 | if (!scheme) { 49 | scheme = @"mailto:"; 50 | } else if (![scheme containsString:@":"]) { 51 | scheme = [scheme stringByAppendingString:@":"]; 52 | } 53 | 54 | scheme = [[scheme stringByAppendingString:@"?test@test.de"] 55 | stringByAddingPercentEncodingWithAllowedCharacters:set]; 56 | 57 | NSURL *url = [[NSURL URLWithString:scheme] absoluteURL]; 58 | 59 | dispatch_semaphore_t sema = dispatch_semaphore_create(0); 60 | 61 | dispatch_async(dispatch_get_main_queue(), ^{ 62 | canOpen = [[UIApplication sharedApplication] canOpenURL:url]; 63 | dispatch_semaphore_signal(sema); 64 | }); 65 | 66 | dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); 67 | 68 | return canOpen; 69 | } 70 | 71 | /** 72 | * Instantiates an email composer view. 73 | * 74 | * @param properties The email properties like subject, body, attachments 75 | * @param delegateTo The mail composition view controller’s delegate. 76 | * 77 | * @return The configured email composer view 78 | */ 79 | - (MFMailComposeViewController*) mailComposerFromProperties:(NSDictionary*)props 80 | delegateTo:(id)receiver 81 | { 82 | BOOL isHTML = [[props objectForKey:@"isHtml"] boolValue]; 83 | 84 | MFMailComposeViewController* draft; 85 | 86 | draft = [[MFMailComposeViewController alloc] init]; 87 | 88 | // Sender 89 | [self setSendingEmailAddress:[props objectForKey:@"from"] ofDraft:draft]; 90 | // Subject 91 | [self setSubject:[props objectForKey:@"subject"] ofDraft:draft]; 92 | // Body (as HTML) 93 | [self setBody:[props objectForKey:@"body"] ofDraft:draft isHTML:isHTML]; 94 | // Recipients 95 | [self setToRecipients:[props objectForKey:@"to"] ofDraft:draft]; 96 | // CC Recipients 97 | [self setCcRecipients:[props objectForKey:@"cc"] ofDraft:draft]; 98 | // BCC Recipients 99 | [self setBccRecipients:[props objectForKey:@"bcc"] ofDraft:draft]; 100 | // Attachments 101 | [self setAttachments:[props objectForKey:@"attachments"] ofDraft:draft]; 102 | 103 | draft.mailComposeDelegate = receiver; 104 | 105 | return draft; 106 | } 107 | 108 | /** 109 | * Creates an mailto-url-sheme. 110 | * 111 | * @param properties The email properties like subject, body, attachments 112 | * 113 | * @return The configured mailto-sheme 114 | */ 115 | - (NSURL*) urlFromProperties:(NSDictionary*)props 116 | { 117 | NSString* mailto = [props objectForKey:@"app"]; 118 | NSMutableArray* parts = [[NSMutableArray alloc] init]; 119 | NSString* query = @""; 120 | 121 | NSString* subject = [props objectForKey:@"subject"]; 122 | NSString* body = [props objectForKey:@"body"]; 123 | NSArray* to = [props objectForKey:@"to"]; 124 | NSArray* cc = [props objectForKey:@"cc"]; 125 | NSArray* bcc = [props objectForKey:@"bcc"]; 126 | NSArray* attachments = [props objectForKey:@"attachments"]; 127 | 128 | NSCharacterSet* cs = [NSCharacterSet URLHostAllowedCharacterSet]; 129 | 130 | if (![mailto containsString:@"://"]) { 131 | mailto = [mailto stringByAppendingString:@"://"]; 132 | } 133 | 134 | if (to.count > 0) { 135 | [parts addObject: [NSString stringWithFormat: @"to=%@", 136 | [to componentsJoinedByString:@","]]]; 137 | } 138 | 139 | if (cc.count > 0) { 140 | [parts addObject: [NSString stringWithFormat: @"cc=%@", 141 | [cc componentsJoinedByString:@","]]]; 142 | } 143 | 144 | if (bcc.count > 0) { 145 | [parts addObject: [NSString stringWithFormat: @"bcc=%@", 146 | [bcc componentsJoinedByString:@","]]]; 147 | } 148 | 149 | if (subject.length > 0) { 150 | [parts addObject: [NSString stringWithFormat: @"subject=%@", 151 | [subject stringByAddingPercentEncodingWithAllowedCharacters:cs]]]; 152 | } 153 | 154 | if (body.length > 0) { 155 | [parts addObject: [NSString stringWithFormat: @"body=%@", 156 | [body stringByAddingPercentEncodingWithAllowedCharacters:cs]]]; 157 | } 158 | 159 | if (attachments.count > 0) { 160 | NSLog(@"The 'mailto' URI Scheme (RFC 2368) does not support attachments."); 161 | } 162 | 163 | query = [parts componentsJoinedByString:@"&"]; 164 | 165 | if (query.length > 0) { 166 | query = [@"?" stringByAppendingString:query]; 167 | } 168 | 169 | mailto = [mailto stringByAppendingString:query]; 170 | 171 | return [NSURL URLWithString:mailto]; 172 | } 173 | 174 | #pragma mark - 175 | #pragma mark Private 176 | 177 | /** 178 | * Sets the subject of the email draft. 179 | * 180 | * @param subject The subject 181 | * @param draft The email composer view 182 | */ 183 | - (void) setSendingEmailAddress:(NSString*)from 184 | ofDraft:(MFMailComposeViewController*)draft 185 | { 186 | if (@available(iOS 11.0, *)) { 187 | [draft setPreferredSendingEmailAddress:from]; 188 | } 189 | } 190 | 191 | /** 192 | * Sets the subject of the email draft. 193 | * 194 | * @param subject The subject 195 | * @param draft The email composer view 196 | */ 197 | - (void) setSubject:(NSString*)subject 198 | ofDraft:(MFMailComposeViewController*)draft 199 | { 200 | [draft setSubject:subject]; 201 | } 202 | 203 | /** 204 | * Sets the body of the email draft. 205 | * 206 | * @param body The body 207 | * @param isHTML Indicates if the body is an HTML encoded string. 208 | * @param draft The email composer view 209 | */ 210 | - (void) setBody:(NSString*)body ofDraft:(MFMailComposeViewController*)draft 211 | isHTML:(BOOL)isHTML 212 | { 213 | [draft setMessageBody:body isHTML:isHTML]; 214 | } 215 | 216 | /** 217 | * Sets the recipients of the email draft. 218 | * 219 | * @param recipients The recipients 220 | * @param draft The email composer view. 221 | */ 222 | - (void) setToRecipients:(NSArray*)recipients 223 | ofDraft:(MFMailComposeViewController*)draft 224 | { 225 | [draft setToRecipients:recipients]; 226 | } 227 | 228 | /** 229 | * Sets the CC recipients of the email draft. 230 | * 231 | * @param ccRecipients The CC recipients 232 | * @param draft The email composer view 233 | */ 234 | - (void) setCcRecipients:(NSArray*)ccRecipients 235 | ofDraft:(MFMailComposeViewController*)draft 236 | { 237 | [draft setCcRecipients:ccRecipients]; 238 | } 239 | 240 | /** 241 | * Sets the BCC recipients of the email draft. 242 | * 243 | * @param bccRecipients The BCC recipients 244 | * @param draft The email composer view. 245 | */ 246 | - (void) setBccRecipients:(NSArray*)bccRecipients 247 | ofDraft:(MFMailComposeViewController*)draft 248 | { 249 | [draft setBccRecipients:bccRecipients]; 250 | } 251 | 252 | /** 253 | * Sets the attachments of the email draft. 254 | * 255 | * @param attachments The attachments 256 | * @param draft The email composer view 257 | */ 258 | - (void) setAttachments:(NSArray*)attatchments 259 | ofDraft:(MFMailComposeViewController*)draft 260 | { 261 | if (!attatchments) return; 262 | 263 | for (NSString* path in attatchments) 264 | { 265 | NSData* data = [self getDataForAttachmentPath:path]; 266 | 267 | if (!data) continue; 268 | 269 | NSString* basename = [self getBasenameFromAttachmentPath:path]; 270 | NSString* pathExt = [basename pathExtension]; 271 | NSString* fileName = [basename pathComponents].lastObject; 272 | NSString* mimeType = [self getMimeTypeFromFileExtension:pathExt]; 273 | 274 | // Couldn't find mimeType, must be some type of binary data 275 | if (mimeType == nil) mimeType = @"application/octet-stream"; 276 | 277 | [draft addAttachmentData:data mimeType:mimeType fileName:fileName]; 278 | } 279 | } 280 | 281 | /** 282 | * Returns the data for a given (relative) attachment path. 283 | * 284 | * @param path An absolute/relative path or the base64 data 285 | * 286 | * @return The data for the attachment. 287 | */ 288 | - (NSData*) getDataForAttachmentPath:(NSString*)path 289 | { 290 | if ([path hasPrefix:@"file:///"]) 291 | { 292 | return [self dataForAbsolutePath:path]; 293 | } 294 | else if ([path hasPrefix:@"res:"]) 295 | { 296 | return [self dataForResource:path]; 297 | } 298 | else if ([path hasPrefix:@"file://"]) 299 | { 300 | return [self dataForAsset:path]; 301 | } 302 | else if ([path hasPrefix:@"app://"]) 303 | { 304 | return [self dataForAppInternalPath:path]; 305 | } 306 | else if ([path hasPrefix:@"base64:"]) 307 | { 308 | return [self dataFromBase64:path]; 309 | } 310 | 311 | NSFileManager* fm = [NSFileManager defaultManager]; 312 | 313 | if (![fm fileExistsAtPath:path]){ 314 | NSLog(@"File not found: %@", path); 315 | } 316 | 317 | return [fm contentsAtPath:path]; 318 | } 319 | 320 | /** 321 | * Retrieves the data for an absolute attachment path. 322 | * 323 | * @param path An absolute file path. 324 | * 325 | * @return The data for the attachment. 326 | */ 327 | - (NSData*) dataForAbsolutePath:(NSString*)path 328 | { 329 | NSFileManager* fm = [NSFileManager defaultManager]; 330 | NSString* absPath; 331 | 332 | absPath = [path stringByReplacingOccurrencesOfString:@"file://" 333 | withString:@""]; 334 | 335 | if (![fm fileExistsAtPath:absPath]) { 336 | NSLog(@"File not found: %@", absPath); 337 | } 338 | 339 | NSData* data = [fm contentsAtPath:absPath]; 340 | 341 | return data; 342 | } 343 | 344 | /** 345 | * Retrieves the data for a resource path. 346 | * 347 | * @param path A relative file path. 348 | * 349 | * @return The data for the attachment. 350 | */ 351 | - (NSData*) dataForResource:(NSString*)path 352 | { 353 | NSString* imgName = [[path pathComponents].lastObject 354 | stringByDeletingPathExtension]; 355 | 356 | #ifdef __CORDOVA_4_0_0 357 | if ([imgName isEqualToString:@"icon"]) { 358 | imgName = @"AppIcon60x60@3x"; 359 | } 360 | #endif 361 | 362 | UIImage* img = [UIImage imageNamed:imgName]; 363 | 364 | if (img == NULL) { 365 | NSLog(@"File not found: %@", path); 366 | } 367 | 368 | NSData* data = UIImagePNGRepresentation(img); 369 | 370 | return data; 371 | } 372 | 373 | /** 374 | * Retrieves the data for a asset path. 375 | * 376 | * @param path A relative www file path. 377 | * 378 | * @return The data for the attachment. 379 | */ 380 | - (NSData*) dataForAsset:(NSString*)path 381 | { 382 | NSFileManager* fm = [NSFileManager defaultManager]; 383 | NSString* absPath; 384 | 385 | NSBundle* mainBundle = [NSBundle mainBundle]; 386 | NSString* bundlePath = [[mainBundle bundlePath] 387 | stringByAppendingString:@"/"]; 388 | 389 | absPath = [path stringByReplacingOccurrencesOfString:@"file:/" 390 | withString:@"www"]; 391 | 392 | absPath = [bundlePath stringByAppendingString:absPath]; 393 | 394 | if (![fm fileExistsAtPath:absPath]) { 395 | NSLog(@"File not found: %@", absPath); 396 | } 397 | 398 | NSData* data = [fm contentsAtPath:absPath]; 399 | 400 | return data; 401 | } 402 | 403 | /** 404 | * Retrieves the file URL for an internal app path. 405 | * 406 | * @param path A relative file path from main bundle dir. 407 | * 408 | * @return The data for the attachment. 409 | */ 410 | - (NSData*) dataForAppInternalPath:(NSString*)path 411 | { 412 | NSFileManager* fm = [NSFileManager defaultManager]; 413 | 414 | NSBundle* mainBundle = [NSBundle mainBundle]; 415 | NSString* absPath = [mainBundle bundlePath]; 416 | 417 | if (![fm fileExistsAtPath:absPath]) { 418 | NSLog(@"File not found: %@", absPath); 419 | } 420 | 421 | NSData* data = [fm contentsAtPath:absPath]; 422 | 423 | return data; 424 | } 425 | 426 | /** 427 | * Retrieves the data for a base64 encoded string. 428 | * 429 | * @param base64String Base64 encoded string. 430 | * 431 | * @return The data for the attachment. 432 | */ 433 | - (NSData*) dataFromBase64:(NSString*)base64String 434 | { 435 | NSUInteger length = [base64String length]; 436 | NSRegularExpression *regex; 437 | NSString *dataString; 438 | 439 | regex = [NSRegularExpression regularExpressionWithPattern:@"^base64:[^/]+.." 440 | options:NSRegularExpressionCaseInsensitive 441 | error:NULL]; 442 | 443 | dataString = [regex stringByReplacingMatchesInString:base64String 444 | options:0 445 | range:NSMakeRange(0, length) 446 | withTemplate:@""]; 447 | 448 | NSData* data = [[NSData alloc] initWithBase64EncodedString:dataString 449 | options:NSDataBase64DecodingIgnoreUnknownCharacters]; 450 | 451 | return data; 452 | } 453 | 454 | /** 455 | * Retrieves the mime type from the file extension. 456 | * 457 | * @param extension The file's extension. 458 | * 459 | * @return The coresponding MIME type. 460 | */ 461 | - (NSString*) getMimeTypeFromFileExtension:(NSString*)extension 462 | { 463 | if (!extension) 464 | return nil; 465 | 466 | // Get the UTI from the file's extension 467 | CFStringRef ext = (CFStringRef)CFBridgingRetain(extension); 468 | CFStringRef type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, ext, NULL); 469 | 470 | // Converting UTI to a mime type 471 | NSString *result = (NSString*) 472 | CFBridgingRelease(UTTypeCopyPreferredTagWithClass(type, kUTTagClassMIMEType)); 473 | 474 | CFRelease(ext); 475 | CFRelease(type); 476 | 477 | return result; 478 | } 479 | 480 | /** 481 | * Retrieves the attachments basename. 482 | * 483 | * @param path The file path or bas64 data of the attachment. 484 | * 485 | * @return The attachments basename. 486 | */ 487 | - (NSString*) getBasenameFromAttachmentPath:(NSString*)path 488 | { 489 | if ([path hasPrefix:@"base64:"]) 490 | { 491 | NSString* pathWithoutPrefix; 492 | 493 | pathWithoutPrefix = [path stringByReplacingOccurrencesOfString:@"base64:" 494 | withString:@""]; 495 | 496 | return [pathWithoutPrefix substringToIndex: 497 | [pathWithoutPrefix rangeOfString:@"//"].location]; 498 | } 499 | 500 | return path; 501 | } 502 | 503 | @end 504 | -------------------------------------------------------------------------------- /src/osx/APPEmailComposer.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import 21 | 22 | @interface APPEmailComposer : CDVPlugin 23 | 24 | // Shows the email composer view with pre-filled data 25 | - (void) open:(CDVInvokedUrlCommand*)command; 26 | // Checks if an email account is configured 27 | - (void) account:(CDVInvokedUrlCommand*)command; 28 | // Checks if the specified email client is installed 29 | - (void) client:(CDVInvokedUrlCommand*)command; 30 | 31 | @end 32 | -------------------------------------------------------------------------------- /src/osx/APPEmailComposer.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import "APPEmailComposer.h" 21 | #import "APPEmailComposerImpl.h" 22 | 23 | @interface APPEmailComposer () 24 | 25 | // Reference is needed because of the async delegate 26 | @property (nonatomic, strong) CDVInvokedUrlCommand* command; 27 | // Implements the core functionality 28 | @property (nonatomic, strong) APPEmailComposerImpl* impl; 29 | 30 | @end 31 | 32 | @implementation APPEmailComposer 33 | 34 | @synthesize command, impl; 35 | 36 | #pragma mark - 37 | #pragma mark Lifecycle 38 | 39 | /** 40 | * Initialize the core impl object which does the main stuff. 41 | */ 42 | - (void) pluginInitialize 43 | { 44 | self.impl = [[APPEmailComposerImpl alloc] init]; 45 | } 46 | 47 | #pragma mark - 48 | #pragma mark Public 49 | 50 | /** 51 | * Checks if an email account is configured. 52 | */ 53 | - (void) account:(CDVInvokedUrlCommand*)cmd 54 | { 55 | [self.commandDelegate runInBackground:^{ 56 | bool res = [self.impl canSendMail]; 57 | CDVPluginResult* result; 58 | 59 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK 60 | messageAsBool:res]; 61 | 62 | [self.commandDelegate sendPluginResult:result 63 | callbackId:cmd.callbackId]; 64 | }]; 65 | } 66 | 67 | /** 68 | * Checks if an email client is available which responds to the scheme. 69 | */ 70 | - (void) client:(CDVInvokedUrlCommand*)cmd 71 | { 72 | [self.commandDelegate runInBackground:^{ 73 | NSString* scheme = [cmd argumentAtIndex:0]; 74 | bool res = [self.impl canOpenScheme:scheme]; 75 | CDVPluginResult* result; 76 | 77 | result = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK 78 | messageAsBool:res]; 79 | 80 | [self.commandDelegate sendPluginResult:result 81 | callbackId:cmd.callbackId]; 82 | }]; 83 | } 84 | 85 | /** 86 | * Show the email composer view with pre-filled data. 87 | */ 88 | - (void) open:(CDVInvokedUrlCommand*)cmd 89 | { 90 | NSDictionary* props = [cmd argumentAtIndex:0]; 91 | 92 | self.command = cmd; 93 | 94 | [self.commandDelegate runInBackground:^{ 95 | NSString* scheme = [props objectForKey:@"app"]; 96 | 97 | if (![self canUseAppleMail:scheme]) { 98 | [self openURLFromProperties:props]; 99 | return; 100 | } 101 | 102 | [self presentMailComposerFromProperties:props]; 103 | }]; 104 | } 105 | 106 | #pragma mark - 107 | #pragma mark NSSharingServicePickerDelegate 108 | 109 | - (void) sharingService:(NSSharingService *)sharingService 110 | didShareItems:(NSArray *)items 111 | { 112 | [self execCallback]; 113 | self.command = NULL; 114 | } 115 | 116 | - (void) sharingService:(NSSharingService *)sharingService 117 | didFailToShareItems:(NSArray *)items 118 | error:(NSError *)error 119 | { 120 | [self sharingService:sharingService didShareItems:items]; 121 | } 122 | 123 | #pragma mark - 124 | #pragma mark Private 125 | 126 | /** 127 | * Displays the email draft. 128 | */ 129 | - (void) presentMailComposerFromProperties:(NSDictionary*)props 130 | { 131 | dispatch_async(dispatch_get_main_queue(), ^{ 132 | NSArray* res = 133 | [self.impl mailComposerFromProperties:props delegateTo:self]; 134 | 135 | NSSharingService* draft = res[0]; 136 | NSAttributedString* body = res[1]; 137 | NSMutableArray* attachments = res[2]; 138 | 139 | if (!draft) { 140 | [self execCallback]; 141 | return; 142 | } 143 | 144 | [attachments insertObject:body atIndex:0]; 145 | 146 | [draft performWithItems:attachments]; 147 | }); 148 | 149 | } 150 | 151 | /** 152 | * Instructs the application to open the specified URL. 153 | */ 154 | - (void) openURLFromProperties:(NSDictionary*)props 155 | { 156 | NSURL* url = [self.impl urlFromProperties:props]; 157 | 158 | dispatch_async(dispatch_get_main_queue(), ^{ 159 | [[NSWorkspace sharedWorkspace] openURL:url]; 160 | }); 161 | } 162 | 163 | /** 164 | * If the specified app if the buil-in iMail framework can be used. 165 | */ 166 | - (BOOL) canUseAppleMail:(NSString*) scheme 167 | { 168 | return [scheme hasPrefix:@"mailto"]; 169 | } 170 | 171 | /** 172 | * Invokes the callback without any parameter. 173 | */ 174 | - (void) execCallback 175 | { 176 | CDVPluginResult *result = [CDVPluginResult 177 | resultWithStatus:CDVCommandStatus_OK]; 178 | 179 | [self.commandDelegate sendPluginResult:result 180 | callbackId:self.command.callbackId]; 181 | } 182 | 183 | @end 184 | -------------------------------------------------------------------------------- /src/osx/APPEmailComposerImpl.h: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | @interface APPEmailComposerImpl : NSObject 21 | 22 | // Checks if the mail composer is able to send mails 23 | - (bool) canSendMail; 24 | // Checks if the mail composer is able to open the specified mail client 25 | - (bool) canOpenScheme:(NSString*)scheme; 26 | // Creates an mailto-url-sheme 27 | - (NSURL*) urlFromProperties:(NSDictionary*)props; 28 | // Instantiates an email composer view 29 | - (NSArray*) mailComposerFromProperties:(NSDictionary*)props 30 | delegateTo:(id)receiver; 31 | 32 | @end 33 | -------------------------------------------------------------------------------- /src/osx/APPEmailComposerImpl.m: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | #import "APPEmailComposerImpl.h" 21 | #import 22 | #import 23 | 24 | @implementation APPEmailComposerImpl 25 | 26 | #pragma mark - 27 | #pragma mark Public 28 | 29 | /** 30 | * Checks if the mail composer is able to send mails. 31 | */ 32 | - (bool) canSendMail 33 | { 34 | NSSharingService* service = 35 | [NSSharingService sharingServiceNamed:NSSharingServiceNameComposeEmail]; 36 | 37 | return [service canPerformWithItems:@[@"Test"]]; 38 | } 39 | 40 | /** 41 | * Checks if an app is available to handle the specified scheme. 42 | * 43 | * @param scheme An URL scheme, that defaults to 'mailto: 44 | */ 45 | - (bool) canOpenScheme:(NSString *)scheme 46 | { 47 | if (!scheme) { 48 | scheme = @"mailto:"; 49 | } else if (![scheme containsString:@":"]) { 50 | scheme = [scheme stringByAppendingString:@":"]; 51 | } 52 | 53 | NSCharacterSet *set = [NSCharacterSet URLFragmentAllowedCharacterSet]; 54 | scheme = [[scheme stringByAppendingString:@"?test@test.de"] 55 | stringByAddingPercentEncodingWithAllowedCharacters:set]; 56 | 57 | NSURL* url = [NSURL URLWithString:scheme]; 58 | NSURL* app = [[NSWorkspace sharedWorkspace] URLForApplicationToOpenURL:url]; 59 | 60 | return app != NULL; 61 | } 62 | 63 | /** 64 | * Instantiates an email composer view. 65 | * 66 | * @param properties The email properties like subject, body, attachments 67 | * @param delegateTo The mail composition view controller’s delegate. 68 | * 69 | * @return The configured email composer view 70 | */ 71 | - (NSArray*) mailComposerFromProperties:(NSDictionary*)props 72 | delegateTo:(id)receiver 73 | { 74 | BOOL isHTML = [[props objectForKey:@"isHtml"] boolValue]; 75 | 76 | NSSharingService* draft = 77 | [NSSharingService sharingServiceNamed:NSSharingServiceNameComposeEmail]; 78 | 79 | draft.subject = [props objectForKey:@"subject"]; 80 | 81 | draft.recipients = [[[props objectForKey:@"to"] 82 | arrayByAddingObjectsFromArray:[props objectForKey:@"cc"]] 83 | arrayByAddingObjectsFromArray:[props objectForKey:@"bcc"]]; 84 | 85 | NSAttributedString* body = 86 | [self getBody:[props objectForKey:@"body"] isHTML:isHTML]; 87 | 88 | NSArray* attachments = 89 | [self getAttachments:[props objectForKey:@"attachments"]]; 90 | 91 | draft.delegate = receiver; 92 | 93 | return @[draft, body, attachments]; 94 | } 95 | 96 | /** 97 | * Creates an mailto-url-sheme. 98 | * 99 | * @param properties The email properties like subject, body, attachments 100 | * 101 | * @return The configured mailto-sheme 102 | */ 103 | - (NSURL*) urlFromProperties:(NSDictionary*)props 104 | { 105 | NSString* mailto = [props objectForKey:@"app"]; 106 | NSMutableArray* parts = [[NSMutableArray alloc] init]; 107 | NSString* query = @""; 108 | 109 | NSString* subject = [props objectForKey:@"subject"]; 110 | NSString* body = [props objectForKey:@"body"]; 111 | NSArray* to = [props objectForKey:@"to"]; 112 | NSArray* cc = [props objectForKey:@"cc"]; 113 | NSArray* bcc = [props objectForKey:@"bcc"]; 114 | NSArray* attachments = [props objectForKey:@"attachments"]; 115 | 116 | NSCharacterSet* cs = [NSCharacterSet URLHostAllowedCharacterSet]; 117 | 118 | if (![mailto containsString:@"://"]) { 119 | mailto = [mailto stringByAppendingString:@"://"]; 120 | } 121 | 122 | if (to.count > 0) { 123 | [parts addObject: [NSString stringWithFormat: @"to=%@", 124 | [to componentsJoinedByString:@","]]]; 125 | } 126 | 127 | if (cc.count > 0) { 128 | [parts addObject: [NSString stringWithFormat: @"cc=%@", 129 | [cc componentsJoinedByString:@","]]]; 130 | } 131 | 132 | if (bcc.count > 0) { 133 | [parts addObject: [NSString stringWithFormat: @"bcc=%@", 134 | [bcc componentsJoinedByString:@","]]]; 135 | } 136 | 137 | if (subject.length > 0) { 138 | [parts addObject: [NSString stringWithFormat: @"subject=%@", 139 | [subject stringByAddingPercentEncodingWithAllowedCharacters:cs]]]; 140 | } 141 | 142 | if (body.length > 0) { 143 | [parts addObject: [NSString stringWithFormat: @"body=%@", 144 | [body stringByAddingPercentEncodingWithAllowedCharacters:cs]]]; 145 | } 146 | 147 | if (attachments.count > 0) { 148 | NSLog(@"The 'mailto' URI Scheme (RFC 2368) does not support attachments."); 149 | } 150 | 151 | query = [parts componentsJoinedByString:@"&"]; 152 | 153 | if (query.length > 0) { 154 | query = [@"?" stringByAppendingString:query]; 155 | } 156 | 157 | mailto = [mailto stringByAppendingString:query]; 158 | 159 | return [NSURL URLWithString:mailto]; 160 | } 161 | 162 | #pragma mark - 163 | #pragma mark Private 164 | 165 | /** 166 | * Sets the body of the email draft. 167 | * 168 | * @param body The body 169 | * @param draft The email composer view 170 | */ 171 | - (NSAttributedString*) getBody:(NSString*)body isHTML:(BOOL)isHTML 172 | { 173 | if (!isHTML) 174 | return [[NSAttributedString alloc] initWithString:body]; 175 | 176 | NSData* html = 177 | [NSData dataWithBytes:[body UTF8String] 178 | length:[body lengthOfBytesUsingEncoding:NSUTF8StringEncoding]]; 179 | 180 | NSString* www = [[NSBundle mainBundle] pathForResource:@"www" ofType:NULL]; 181 | NSURL* baseURL = [NSURL fileURLWithPath:www]; 182 | 183 | return [[NSAttributedString alloc] initWithHTML:html 184 | baseURL:baseURL 185 | documentAttributes:NULL]; 186 | } 187 | 188 | /** 189 | * Sets the attachments of the email draft. 190 | * 191 | * @param attachments The attachments 192 | * @param draft The email composer view 193 | */ 194 | - (NSArray*) getAttachments:(NSArray*)attatchments 195 | { 196 | NSMutableArray* uris = [[NSMutableArray alloc] init]; 197 | 198 | if (!attatchments) 199 | return uris; 200 | 201 | for (NSString* path in attatchments) 202 | { 203 | NSURL* url = [self urlForAttachmentPath:path]; 204 | 205 | [uris addObject:url]; 206 | } 207 | 208 | return uris; 209 | } 210 | 211 | /** 212 | * Returns the URL for a given (relative) attachment path. 213 | * 214 | * @param path An absolute/relative path or the base64 data 215 | * 216 | * @return The URL for the attachment. 217 | */ 218 | - (NSURL*) urlForAttachmentPath:(NSString*)path 219 | { 220 | if ([path hasPrefix:@"file:///"]) 221 | { 222 | return [self urlForAbsolutePath:path]; 223 | } 224 | else if ([path hasPrefix:@"res:"]) 225 | { 226 | return [self urlForResource:path]; 227 | } 228 | else if ([path hasPrefix:@"file://"]) 229 | { 230 | return [self urlForAsset:path]; 231 | } 232 | else if ([path hasPrefix:@"app://"]) 233 | { 234 | return [self urlForAppInternalPath:path]; 235 | } 236 | else if ([path hasPrefix:@"base64:"]) 237 | { 238 | return [self urlFromBase64:path]; 239 | } 240 | 241 | NSFileManager* fm = [NSFileManager defaultManager]; 242 | 243 | if (![fm fileExistsAtPath:path]){ 244 | NSLog(@"File not found: %@", path); 245 | } 246 | 247 | return [NSURL fileURLWithPath:path]; 248 | } 249 | 250 | /** 251 | * Retrieves the data for an absolute attachment path. 252 | * 253 | * @param path An absolute file path. 254 | * 255 | * @return The data for the attachment. 256 | */ 257 | - (NSURL*) urlForAbsolutePath:(NSString*)path 258 | { 259 | NSFileManager* fm = [NSFileManager defaultManager]; 260 | NSString* absPath; 261 | 262 | absPath = [path stringByReplacingOccurrencesOfString:@"file://" 263 | withString:@""]; 264 | 265 | if (![fm fileExistsAtPath:absPath]) { 266 | NSLog(@"File not found: %@", absPath); 267 | } 268 | 269 | return [NSURL fileURLWithPath:absPath]; 270 | } 271 | 272 | /** 273 | * Retrieves the data for a resource path. 274 | * 275 | * @param path A relative file path. 276 | * 277 | * @return The data for the attachment. 278 | */ 279 | - (NSURL*) urlForResource:(NSString*)path 280 | { 281 | NSFileManager* fm = [NSFileManager defaultManager]; 282 | NSString* absPath; 283 | 284 | NSBundle* mainBundle = [NSBundle mainBundle]; 285 | NSString* bundlePath = [[mainBundle bundlePath] 286 | stringByAppendingString:@"/Contents/Resources/"]; 287 | 288 | if ([path hasPrefix:@"res://icon"]) { 289 | path = @"res://AppIcon.icns"; 290 | } 291 | 292 | absPath = [path stringByReplacingOccurrencesOfString:@"res://" 293 | withString:@""]; 294 | 295 | absPath = [bundlePath stringByAppendingString:absPath]; 296 | 297 | if (![fm fileExistsAtPath:absPath]) { 298 | NSLog(@"File not found: %@", absPath); 299 | } 300 | 301 | return [NSURL fileURLWithPath:absPath]; 302 | } 303 | 304 | /** 305 | * Retrieves the file URL for a asset path. 306 | * 307 | * @param path A relative www file path. 308 | * 309 | * @return The URL to the attachment. 310 | */ 311 | - (NSURL*) urlForAsset:(NSString*)path 312 | { 313 | NSFileManager* fm = [NSFileManager defaultManager]; 314 | NSString* absPath; 315 | 316 | NSBundle* mainBundle = [NSBundle mainBundle]; 317 | NSString* bundlePath = [[mainBundle bundlePath] 318 | stringByAppendingString:@"/Contents/Resources/"]; 319 | 320 | absPath = [path stringByReplacingOccurrencesOfString:@"file:/" 321 | withString:@"www"]; 322 | 323 | absPath = [bundlePath stringByAppendingString:absPath]; 324 | 325 | if (![fm fileExistsAtPath:absPath]) { 326 | NSLog(@"File not found: %@", absPath); 327 | } 328 | 329 | return [NSURL fileURLWithPath:absPath]; 330 | } 331 | 332 | /** 333 | * Retrieves the file URL for an internal app path. 334 | * 335 | * @param path A relative file path from main bundle dir. 336 | * 337 | * @return The URL for the internal path. 338 | */ 339 | - (NSURL*) urlForAppInternalPath:(NSString*)path 340 | { 341 | NSFileManager* fm = [NSFileManager defaultManager]; 342 | 343 | NSBundle* mainBundle = [NSBundle mainBundle]; 344 | NSString* absPath = [mainBundle bundlePath]; 345 | 346 | if (![fm fileExistsAtPath:absPath]) { 347 | NSLog(@"File not found: %@", absPath); 348 | } 349 | 350 | return [NSURL fileURLWithPath:absPath]; 351 | } 352 | 353 | /** 354 | * Retrieves the data for a base64 encoded string. 355 | * 356 | * @param base64String Base64 encoded string. 357 | * 358 | * @return The data for the attachment. 359 | */ 360 | - (NSURL*) urlFromBase64:(NSString*)base64String 361 | { 362 | NSString *filename = [self getBasenameFromAttachmentPath:base64String]; 363 | NSUInteger length = [base64String length]; 364 | NSRegularExpression *regex; 365 | NSString *dataString; 366 | 367 | regex = [NSRegularExpression regularExpressionWithPattern:@"^base64:[^/]+.." 368 | options:NSRegularExpressionCaseInsensitive 369 | error:Nil]; 370 | 371 | dataString = [regex stringByReplacingMatchesInString:base64String 372 | options:0 373 | range:NSMakeRange(0, length) 374 | withTemplate:@""]; 375 | 376 | NSData* data = [[NSData alloc] initWithBase64EncodedString:dataString 377 | options:0]; 378 | 379 | 380 | return [self urlForData:data withFileName:filename]; 381 | } 382 | 383 | /** 384 | * Retrieves the attachments basename. 385 | * 386 | * @param path The file path or bas64 data of the attachment. 387 | * 388 | * @return The attachments basename. 389 | */ 390 | - (NSString*) getBasenameFromAttachmentPath:(NSString*)path 391 | { 392 | if ([path hasPrefix:@"base64:"]) 393 | { 394 | NSString* pathWithoutPrefix; 395 | 396 | pathWithoutPrefix = [path stringByReplacingOccurrencesOfString:@"base64:" 397 | withString:@""]; 398 | 399 | return [pathWithoutPrefix substringToIndex: 400 | [pathWithoutPrefix rangeOfString:@"//"].location]; 401 | } 402 | 403 | return path; 404 | } 405 | 406 | /** 407 | * Write the data into a temp file. 408 | * 409 | * @param data The data to save into a file. 410 | * @param name The name of the file. 411 | * 412 | * @return The file URL 413 | */ 414 | - (NSURL*) urlForData:(NSData*)data withFileName:(NSString*) filename 415 | { 416 | NSFileManager* fm = [NSFileManager defaultManager]; 417 | 418 | NSString* tempDir = NSTemporaryDirectory(); 419 | 420 | [fm createDirectoryAtPath:tempDir withIntermediateDirectories:YES 421 | attributes:NULL 422 | error:NULL]; 423 | 424 | NSString* absPath = [tempDir stringByAppendingPathComponent:filename]; 425 | 426 | NSURL* url = [NSURL fileURLWithPath:absPath]; 427 | [data writeToURL:url atomically:NO]; 428 | 429 | if (![fm fileExistsAtPath:absPath]) { 430 | NSLog(@"File not found: %@", absPath); 431 | } 432 | 433 | return url; 434 | } 435 | 436 | @end 437 | -------------------------------------------------------------------------------- /src/windows/EmailComposerProxy.js: -------------------------------------------------------------------------------- 1 | /* globals Windows: true */ 2 | 3 | /* 4 | Licensed to the Apache Software Foundation (ASF) under one 5 | or more contributor license agreements. See the NOTICE file 6 | distributed with this work for additional information 7 | regarding copyright ownership. The ASF licenses this file 8 | to you under the Apache License, Version 2.0 (the 9 | "License"); you may not use this file except in compliance 10 | with the License. You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, 15 | software distributed under the License is distributed on an 16 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 17 | KIND, either express or implied. See the License for the 18 | specific language governing permissions and limitations 19 | under the License. 20 | */ 21 | 22 | var WinLauncher = Windows.System.Launcher, 23 | WinMail = Windows.ApplicationModel.Email; 24 | 25 | /** 26 | * Tries to find out if the device has an configured email account. 27 | * 28 | * @param [ Function ] success Success callback 29 | * @param [ Function ] error Error callback 30 | * @param [ Array ] args Interface arguments 31 | * 32 | * @return [ Void ] 33 | */ 34 | exports.account = function (success, error, args) { 35 | success(null); 36 | }; 37 | 38 | /** 39 | * Tries to find out if the device has an installed email client. 40 | * 41 | * @param [ Function ] success Success callback 42 | * @param [ Function ] error Error callback 43 | * @param [ Array ] args Interface arguments 44 | * 45 | * @return [ Void ] 46 | */ 47 | exports.client = function (success, error, args) { 48 | success(args[0] === 'mailto:' ? true : null); 49 | }; 50 | 51 | /** 52 | * Displays the email composer pre-filled with data. 53 | * 54 | * @param [ Function ] success Success callback 55 | * @param [ Function ] error Error callback 56 | * @param [ Array ] args Interface arguments 57 | * 58 | * @return [ Void ] 59 | */ 60 | exports.open = function (success, error, args) { 61 | var props = args[0], 62 | impl = exports.impl; 63 | 64 | if (WinMail) { 65 | impl.getDraftWithProperties(props) 66 | .then(WinMail.EmailManager.showComposeNewEmailAsync) 67 | .done(success, error); 68 | } else { 69 | var mailTo = impl.getMailTo(props); 70 | 71 | WinLauncher 72 | .launchUriAsync(mailTo) 73 | .done(success, error); 74 | } 75 | }; 76 | 77 | require('cordova/exec/proxy').add('EmailComposer', exports); 78 | -------------------------------------------------------------------------------- /src/windows/EmailComposerProxyImpl.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | var proxy = require('cordova-plugin-email-composer.EmailComposerProxy'), 21 | impl = proxy.impl = {}, 22 | WinMail = Windows.ApplicationModel.Email; 23 | 24 | /** 25 | * The Email with the containing properties. 26 | * 27 | * @param [ Object ] props Properties like subject. 28 | * 29 | * @return [ Email.EmailMessage ] 30 | */ 31 | impl.getDraftWithProperties = function (props) { 32 | var me = this; 33 | 34 | return new WinJS.Promise(function (complete) { 35 | var mail = new WinMail.EmailMessage(); 36 | 37 | // From sender 38 | me.setSendingEmailAddress(props.from, mail); 39 | // Subject 40 | me.setSubject(props.subject, mail); 41 | // Body 42 | me.setBody(props.body, props.isHtml, mail); 43 | // To recipients 44 | me.setRecipients(props.to, mail.to); 45 | // CC recipients 46 | me.setRecipients(props.cc, mail.cc); 47 | // BCC recipients 48 | me.setRecipients(props.bcc, mail.bcc); 49 | // attachments 50 | me.setAttachments(props.attachments, mail) 51 | 52 | .then(function () { 53 | complete(mail); 54 | }); 55 | }); 56 | }; 57 | 58 | /** 59 | * Construct a mailto: string based on the provided properties. 60 | * 61 | * @param [ Object ] props Properties like subject. 62 | * 63 | * @return [ Windows.Foundation.Uri ] 64 | */ 65 | impl.getMailTo = function (props) { 66 | var uri = 'mailto:' + props.to, 67 | query = ''; 68 | 69 | if (props.subject !== '') { 70 | query = query + '&subject=' + props.subject; 71 | } 72 | if (props.body !== '') { 73 | query = query + '&body=' + props.body; 74 | } 75 | if (props.cc !== '') { 76 | query = query + '&cc=' + props.cc; 77 | } 78 | if (props.bcc !== '') { 79 | query = query + '&bcc=' + props.bcc; 80 | } 81 | if (query !== '') { 82 | query = '?' + query.substring(1); 83 | uri = uri + query; 84 | } 85 | 86 | return new Windows.Foundation.Uri(uri); 87 | }; 88 | 89 | /** 90 | * Setter for the subject. 91 | * 92 | * @param [ String ] subject 93 | * @param [ Email.EmailMessage ] draft 94 | * 95 | * @return [ Void ] 96 | */ 97 | impl.setSubject = function (subject, draft) { 98 | draft.subject = subject; 99 | }; 100 | 101 | /** 102 | * Setter for the body. 103 | * 104 | * @param [ String ] body The email body. 105 | * @param [ Boolean ] isHTML Indicates the encoding (HTML or plain text) 106 | * @param [ Email.EmailMessage ] draft 107 | * 108 | * @return [ Void ] 109 | */ 110 | impl.setBody = function (body, isHTML, draft) { 111 | draft.body = body; 112 | }; 113 | 114 | /** 115 | * Setter for the sending email address. 116 | * 117 | * @param [ String ] from The sending email address. 118 | * @param [ Email.EmailMessage ] draft 119 | * 120 | * @return [ Void ] 121 | */ 122 | impl.setSendingEmailAddress = function (from, draft) { 123 | draft.sender = new WinMail.EmailRecipient(from); 124 | }; 125 | 126 | /** 127 | * Setter for the recipients. 128 | * 129 | * @param [ Array ] recipients List of emails 130 | * @param [ Email.EmailMessage ] draft 131 | * 132 | * @return [ Void ] 133 | */ 134 | impl.setRecipients = function (recipients, draft) { 135 | recipients.forEach(function (address) { 136 | draft.push(new WinMail.EmailRecipient(address)); 137 | }); 138 | }; 139 | 140 | /** 141 | * Setter for the attachments. 142 | * 143 | * @param [ Array ] attachments List of uris 144 | * @param [ Email.EmailMessage ] draft 145 | * 146 | * @return [ Void ] 147 | */ 148 | impl.setAttachments = function (attachments, draft) { 149 | var promises = [], me = this; 150 | 151 | return new WinJS.Promise(function (complete) { 152 | attachments.forEach(function (path) { 153 | promises.push(me.getUriForPath(path)); 154 | }); 155 | 156 | WinJS.Promise.thenEach(promises, function (uri) { 157 | draft.attachments.push( 158 | new WinMail.EmailAttachment( 159 | uri.path.split('/').reverse()[0], 160 | Windows.Storage.Streams.RandomAccessStreamReference.createFromUri(uri) 161 | ) 162 | ); 163 | }).done(complete); 164 | }); 165 | }; 166 | 167 | /** 168 | * The URI for an attachment path. 169 | * 170 | * @param [ String ] path The path to the attachment. 171 | * 172 | * @return [ Windows.Foundation.Uri ] 173 | */ 174 | impl.getUriForPath = function (path) { 175 | var me = this; 176 | 177 | return new WinJS.Promise(function (complete) { 178 | if (path.match(/^res:/)) { 179 | complete(me.getUriForResourcePath(path)); 180 | } else if (path.match(/^file:\/{3}/)) { 181 | complete(me.getUriForAbsolutePath(path)); 182 | } else if (path.match(/^file:/)) { 183 | complete(me.getUriForAssetPath(path)); 184 | } else if (path.match(/^app:/)) { 185 | complete(me.getUriForAppInternalPath(path)); 186 | } else if (path.match(/^base64:/)) { 187 | me.getUriFromBase64(path).then(complete); 188 | } else { 189 | complete(new Windows.Foundation.Uri(path)); 190 | } 191 | }); 192 | }; 193 | 194 | /** 195 | * The URI for a file. 196 | * 197 | * @param [ String ] path Absolute path to the attachment. 198 | * 199 | * @return [ Windows.Foundation.Uri ] 200 | */ 201 | impl.getUriForAbsolutePath = function (path) { 202 | return new Windows.Foundation.Uri(path); 203 | }; 204 | 205 | /** 206 | * The URI for an asset. 207 | * 208 | * @param [ String ] path Asset path to the attachment. 209 | * 210 | * @return [ Windows.Foundation.Uri ] 211 | */ 212 | impl.getUriForAssetPath = function (path) { 213 | var resPath = path.replace('file:/', '/www'); 214 | 215 | return this.getUriForPathUtil(resPath); 216 | }; 217 | 218 | /** 219 | * The URI for a resource. 220 | * 221 | * @param [ String ] path Relative path to the attachment. 222 | * 223 | * @return [ Windows.Foundation.Uri ] 224 | */ 225 | impl.getUriForResourcePath = function (path) { 226 | var resPath = path.replace('res:/', '/images'); 227 | 228 | return this.getUriForPathUtil(resPath); 229 | }; 230 | 231 | /** 232 | * The URI for an app internal file. 233 | * 234 | * @param [ String ] path Relative path to the app root dir. 235 | * 236 | * @return [ Windows.Foundation.Uri ] 237 | */ 238 | impl.getUriForAppInternalPath = function (path) { 239 | var resPath = path.replace('app:/', '/'); 240 | 241 | return this.getUriForPathUtil(resPath); 242 | }; 243 | 244 | /** 245 | * The URI for a path. 246 | * 247 | * @param [ String ] path Relative path to the attachment. 248 | * 249 | * @return [ Windows.Foundation.Uri ] 250 | */ 251 | impl.getUriForPathUtil = function (resPath) { 252 | var rawUri = 'ms-appx:' + '//' + resPath; 253 | 254 | return new Windows.Foundation.Uri(rawUri); 255 | }; 256 | 257 | /** 258 | * The URI for a base64 encoded content. 259 | * 260 | * @param [ String ] content Base64 encoded content. 261 | * 262 | * @return [ Windows.Foundation.Uri ] 263 | */ 264 | impl.getUriFromBase64 = function (content) { 265 | return new WinJS.Promise(function (complete) { 266 | var match = content.match(/^base64:([^\/]+)\/\/(.*)/), 267 | base64 = match[2], 268 | name = match[1], 269 | buffer = Windows.Security.Cryptography.CryptographicBuffer.decodeFromBase64String(base64), 270 | rwplus = Windows.Storage.CreationCollisionOption.openIfExists, 271 | folder = Windows.Storage.ApplicationData.current.temporaryFolder, 272 | uri = new Windows.Foundation.Uri('ms-appdata:///temp/' + name); 273 | 274 | folder.createFileAsync(name, rwplus).done(function (file) { 275 | Windows.Storage.FileIO.writeBufferAsync(file, buffer).then(function () { 276 | complete(uri); 277 | }); 278 | }); 279 | }); 280 | }; 281 | -------------------------------------------------------------------------------- /test/readme.md: -------------------------------------------------------------------------------- 1 | To test this plugin create a new repo from scratch 2 | 3 | ``` 4 | cordova create test-email-composer com.test.email.composer testEmailComposer 5 | cd test-email-composer 6 | cordova platform add android|ios 7 | cordova plugin add https://github.com/katzer/cordova-plugin-email-composer.git 8 | ``` 9 | 10 | In `www/index.html` add the following line right after the `

` tag containing `Device is Ready` 11 | 12 | ```html 13 |


14 | 15 | ``` 16 | 17 | In `www/js/index.js` add the following at the end of the function `onDeviceReady` 18 | 19 | ```js 20 | document.getElementById('test-email-composer') 21 | .addEventListener('click', function (event) { 22 | cordova.plugins.email.open({ 23 | from: 'from_test@test.com', // sending email account (iOS only) 24 | to: ['to_test@test.com'], // email addresses for TO field 25 | cc: ['cc_test@test.com'], // email addresses for CC field 26 | bcc: ['bcc_test@test.com'], // email addresses for BCC field 27 | subject: 'teste', // subject of the email 28 | attachments: ['base64:icon.png//iVBORw0KGgoAAAANSUhEUgAAADwAAAA8CAYAAAA6'], 29 | body: 'teste' // email body 30 | }); 31 | }) 32 | ``` 33 | 34 | Then connect your device with a USB cable and run 35 | 36 | ``` 37 | cordova run android --device 38 | ``` 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /www/email_composer.js: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | var exec = require('cordova/exec'), 21 | ua = navigator.userAgent.toLowerCase(), 22 | isAndroid = !window.Windows && ua.indexOf('android') > -1, 23 | mailto = 'mailto:'; 24 | 25 | /** 26 | * List of all registered mail app aliases. 27 | */ 28 | exports.aliases = { 29 | gmail: isAndroid ? 'com.google.android.gm' : 'googlegmail://co', 30 | outlook: isAndroid ? 'com.microsoft.office.outlook' : 'ms-outlook://compose', 31 | hub: isAndroid ? 'com.blackberry.hub' : undefined 32 | }; 33 | 34 | /** 35 | * List of possible permissions to request. 36 | */ 37 | exports.permission = { 38 | READ_EXTERNAL_STORAGE: 1, 39 | READ_ACCOUNTS: 2 40 | }; 41 | 42 | /** 43 | * List of all available options with their default value. 44 | * 45 | * @return [ Object ] 46 | */ 47 | exports.getDefaults = function () { 48 | return { 49 | app: mailto, 50 | from: '', 51 | subject: '', 52 | body: '', 53 | to: [], 54 | cc: [], 55 | bcc: [], 56 | attachments: [], 57 | isHtml: false, 58 | chooserHeader: 'Open with' 59 | }; 60 | }; 61 | 62 | /** 63 | * Informs if the app has the needed permission. 64 | * 65 | * @param [ Number ] permission The permission to check. 66 | * @param [ Function ] callback The callback function. 67 | * @param [ Object ] scope The scope of the callback. 68 | * 69 | * @return [ Void ] 70 | */ 71 | exports.hasPermission = function(permission, callback, scope) { 72 | var fn = this.createCallbackFn(callback, scope); 73 | 74 | if (!isAndroid) { 75 | if (fn) fn(true); 76 | return; 77 | } 78 | 79 | exec(fn, null, 'EmailComposer', 'check', [permission]); 80 | }; 81 | 82 | /** 83 | * Request permission if not already granted. 84 | * 85 | * @param [ Number ] permission The permission to request. 86 | * @param [ Function ] callback The callback function. 87 | * @param [ Object ] scope The scope of the callback. 88 | * 89 | * @return [ Void ] 90 | */ 91 | exports.requestPermission = function(permission, callback, scope) { 92 | var fn = this.createCallbackFn(callback, scope); 93 | 94 | if (!isAndroid) { 95 | if (fn) fn(true); 96 | return; 97 | } 98 | 99 | exec(fn, null, 'EmailComposer', 'request', [permission]); 100 | }; 101 | 102 | /** 103 | * Tries to find out if the device has an configured email account. 104 | * 105 | * @param [ Function ] callback The callback function. 106 | * @param [ Object ] scope The scope of the callback. 107 | * 108 | * @return [ Void ] 109 | */ 110 | exports.hasAccount = function (callback, scope) { 111 | var fn = this.createCallbackFn(callback, scope); 112 | 113 | exec(fn, null, 'EmailComposer', 'account', []); 114 | }; 115 | 116 | /** 117 | * Tries to find out if the device has an installed email client. 118 | * 119 | * @param [ String ] app An optional app id or uri scheme. 120 | * Defaults to mailto. 121 | * @param [ Function ] callback The callback function. 122 | * @param [ Object ] scope The scope of the callback. 123 | * 124 | * @return [ Void ] 125 | */ 126 | exports.hasClient = function (app, callback, scope) { 127 | 128 | if (typeof callback != 'function') { 129 | scope = null; 130 | callback = app; 131 | app = mailto; 132 | } 133 | 134 | var fn = this.createCallbackFn(callback, scope), 135 | app = app || mailto; 136 | 137 | if (this.aliases.hasOwnProperty(app)) { 138 | app = this.aliases[app]; 139 | } 140 | 141 | exec(fn, null, 'EmailComposer', 'client', [app]); 142 | }; 143 | 144 | /** 145 | * List of package IDs for all available email clients (Android only). 146 | * 147 | * @param [ Function ] callback The callback function. 148 | * @param [ Object ] scope The scope of the callback. 149 | * 150 | * @return [ Void ] 151 | */ 152 | exports.getClients = function (callback, scope) { 153 | var fn = this.createCallbackFn(callback, scope); 154 | 155 | if (!isAndroid) { 156 | if (fn) fn(null); 157 | return; 158 | } 159 | 160 | exec(fn, null, 'EmailComposer', 'clients', []); 161 | }; 162 | 163 | /** 164 | * Displays the email composer pre-filled with data. 165 | * 166 | * @param [ Object ] options The email properties like the body,... 167 | * @param [ Function ] callback The callback function. 168 | * @param [ Object ] scope The scope of the callback. 169 | * 170 | * @return [ Void ] 171 | */ 172 | exports.open = function (options, callback, scope) { 173 | 174 | if (typeof options == 'function') { 175 | scope = callback; 176 | callback = options; 177 | options = {}; 178 | } 179 | 180 | var fn = this.createCallbackFn(callback, scope), 181 | options = this.mergeWithDefaults(options || {}); 182 | 183 | if (!isAndroid && options.app != mailto && fn) { 184 | this.registerCallbackForScheme(fn); 185 | } 186 | 187 | exec(fn, null, 'EmailComposer', 'open', [options]); 188 | }; 189 | 190 | /** 191 | * Adds a new mail app alias. 192 | * 193 | * @param [ String ] alias The alias name. 194 | * @param [ String ] packageName The package name. 195 | * 196 | * @return [ Void ] 197 | */ 198 | exports.addAlias = function (alias, packageName) { 199 | this.aliases[alias] = packageName; 200 | }; 201 | 202 | /** 203 | * Alias für `open()`. 204 | */ 205 | exports.openDraft = function () { 206 | this.open.apply(this, arguments); 207 | }; 208 | 209 | /** 210 | * @private 211 | * 212 | * Merge settings with default values. 213 | * 214 | * @param [ Object ] options The custom options 215 | * 216 | * @retrun [ Object ] Default values merged with custom values. 217 | */ 218 | exports.mergeWithDefaults = function (options) { 219 | var defaults = this.getDefaults(); 220 | 221 | if (!options.hasOwnProperty('isHtml')) { 222 | options.isHtml = defaults.isHtml; 223 | } 224 | 225 | if (options.hasOwnProperty('app')) { 226 | options.app = this.aliases[options.app]; 227 | } 228 | 229 | if (Array.isArray(options.body)) { 230 | options.body = options.body.join("\n"); 231 | } 232 | 233 | options.app = String(options.app || defaults.app); 234 | options.from = String(options.from || defaults.from); 235 | options.subject = String(options.subject || defaults.subject); 236 | options.body = String(options.body || defaults.body); 237 | options.chooserHeader = String(options.chooserHeader || defaults.chooserHeader); 238 | options.to = options.to || defaults.to; 239 | options.cc = options.cc || defaults.cc; 240 | options.bcc = options.bcc || defaults.bcc; 241 | options.attachments = options.attachments || defaults.attachments; 242 | options.isHtml = !!options.isHtml; 243 | 244 | if (!Array.isArray(options.to)) { 245 | options.to = [options.to]; 246 | } 247 | 248 | if (!Array.isArray(options.cc)) { 249 | options.cc = [options.cc]; 250 | } 251 | 252 | if (!Array.isArray(options.bcc)) { 253 | options.bcc = [options.bcc]; 254 | } 255 | 256 | if (!Array.isArray(options.attachments)) { 257 | options.attachments = [options.attachments]; 258 | } 259 | 260 | return options; 261 | }; 262 | 263 | /** 264 | * @private 265 | * 266 | * Creates a callback, which will be executed 267 | * within a specific scope. 268 | * 269 | * @param [ Function ] callback The callback function. 270 | * @param [ Object ] scope The scope for the function. 271 | * 272 | * @return [ Function ] The new callback function 273 | */ 274 | exports.createCallbackFn = function (callback, scope) { 275 | 276 | if (typeof callback !== 'function') 277 | return; 278 | 279 | return function () { 280 | callback.apply(scope || this, arguments); 281 | }; 282 | }; 283 | 284 | /** 285 | * @private 286 | * 287 | * Register an Eventlistener on resume-Event to 288 | * execute callback after open a draft. 289 | * 290 | * @return [ Void ] 291 | */ 292 | exports.registerCallbackForScheme = function (fn) { 293 | 294 | var callback = function () { 295 | fn(); 296 | document.removeEventListener('resume',callback); 297 | }; 298 | 299 | document.addEventListener('resume', callback, false); 300 | }; 301 | --------------------------------------------------------------------------------