From 40eb729533d197ba45ba17010ac21b2cc5288ee4 Mon Sep 17 00:00:00 2001 From: Adam Wilson Date: Wed, 8 Nov 2017 12:41:42 +0000 Subject: [PATCH 1/5] Rebase mult-attachment PR --- RNMail/RNMail.m | 101 +++++++++--------- .../java/com/chirag/RNMail/RNMailModule.java | 43 ++++---- 2 files changed, 76 insertions(+), 68 deletions(-) diff --git a/RNMail/RNMail.m b/RNMail/RNMail.m index 605c373..1466e4a 100644 --- a/RNMail/RNMail.m +++ b/RNMail/RNMail.m @@ -41,7 +41,7 @@ + (BOOL)requiresMainQueueSetup NSString *subject = [RCTConvert NSString:options[@"subject"]]; [mail setSubject:subject]; } - + bool *isHTML = NO; if (options[@"isHTML"]){ @@ -57,7 +57,7 @@ + (BOOL)requiresMainQueueSetup NSArray *recipients = [RCTConvert NSArray:options[@"recipients"]]; [mail setToRecipients:recipients]; } - + if (options[@"ccRecipients"]){ NSArray *ccRecipients = [RCTConvert NSArray:options[@"ccRecipients"]]; [mail setCcRecipients:ccRecipients]; @@ -67,49 +67,52 @@ + (BOOL)requiresMainQueueSetup NSArray *bccRecipients = [RCTConvert NSArray:options[@"bccRecipients"]]; [mail setBccRecipients:bccRecipients]; } - - if (options[@"attachment"] && options[@"attachment"][@"path"] && options[@"attachment"][@"type"]){ - NSString *attachmentPath = [RCTConvert NSString:options[@"attachment"][@"path"]]; - NSString *attachmentType = [RCTConvert NSString:options[@"attachment"][@"type"]]; - NSString *attachmentName = [RCTConvert NSString:options[@"attachment"][@"name"]]; - - // Set default filename if not specificed - if (!attachmentName) { - attachmentName = [[attachmentPath lastPathComponent] stringByDeletingPathExtension]; - } - - // Get the resource path and read the file using NSData - NSData *fileData = [NSData dataWithContentsOfFile:attachmentPath]; - - // Determine the MIME type - NSString *mimeType; - - /* - * Add additional mime types and PR if necessary. Find the list - * of supported formats at http://www.iana.org/assignments/media-types/media-types.xhtml - */ - if ([attachmentType isEqualToString:@"jpg"]) { - mimeType = @"image/jpeg"; - } else if ([attachmentType isEqualToString:@"png"]) { - mimeType = @"image/png"; - } else if ([attachmentType isEqualToString:@"doc"]) { - mimeType = @"application/msword"; - } else if ([attachmentType isEqualToString:@"ppt"]) { - mimeType = @"application/vnd.ms-powerpoint"; - } else if ([attachmentType isEqualToString:@"html"]) { - mimeType = @"text/html"; - } else if ([attachmentType isEqualToString:@"csv"]) { - mimeType = @"text/csv"; - } else if ([attachmentType isEqualToString:@"pdf"]) { - mimeType = @"application/pdf"; - } else if ([attachmentType isEqualToString:@"vcard"]) { - mimeType = @"text/vcard"; - } else if ([attachmentType isEqualToString:@"json"]) { - mimeType = @"application/json"; - } else if ([attachmentType isEqualToString:@"zip"]) { - mimeType = @"application/zip"; - } else if ([attachmentType isEqualToString:@"text"]) { - mimeType = @"text/*"; + + if (options[@"attachments"]){ + NSArray *attachments = [RCTConvert NSArray:options[@"attachments"]]; + + for(NSDictionary *attachment in attachments) { + if (attachment[@"path"] && attachment[@"type"]) { + NSString *attachmentPath = [RCTConvert NSString:attachment[@"path"]]; + NSString *attachmentType = [RCTConvert NSString:attachment[@"type"]]; + NSString *attachmentName = [RCTConvert NSString:attachment[@"name"]]; + + // Set default filename if not specificed + if (!attachmentName) { + attachmentName = [[attachmentPath lastPathComponent] stringByDeletingPathExtension]; + } + // Get the resource path and read the file using NSData + NSData *fileData = [NSData dataWithContentsOfFile:attachmentPath]; + + // Determine the MIME type + NSString *mimeType; + + /* + * Add additional mime types and PR if necessary. Find the list + * of supported formats at http://www.iana.org/assignments/media-types/media-types.xhtml + */ + if ([attachmentType isEqualToString:@"jpg"]) { + mimeType = @"image/jpeg"; + } else if ([attachmentType isEqualToString:@"png"]) { + mimeType = @"image/png"; + } else if ([attachmentType isEqualToString:@"doc"]) { + mimeType = @"application/msword"; + } else if ([attachmentType isEqualToString:@"ppt"]) { + mimeType = @"application/vnd.ms-powerpoint"; + } else if ([attachmentType isEqualToString:@"html"]) { + mimeType = @"text/html"; + } else if ([attachmentType isEqualToString:@"csv"]) { + mimeType = @"text/csv"; + } else if ([attachmentType isEqualToString:@"pdf"]) { + mimeType = @"application/pdf"; + } else if ([attachmentType isEqualToString:@"vcard"]) { + mimeType = @"text/vcard"; + } else if ([attachmentType isEqualToString:@"json"]) { + mimeType = @"application/json"; + } else if ([attachmentType isEqualToString:@"zip"]) { + mimeType = @"application/zip"; + } else if ([attachmentType isEqualToString:@"text"]) { + mimeType = @"text/*"; } else if ([attachmentType isEqualToString:@"mp3"]) { mimeType = @"audio/mpeg"; } else if ([attachmentType isEqualToString:@"wav"]) { @@ -122,12 +125,12 @@ + (BOOL)requiresMainQueueSetup mimeType = @"audio/ogg"; } else if ([attachmentType isEqualToString:@"xls"]) { mimeType = @"application/vnd.ms-excel"; + } + [mail addAttachmentData:fileData mimeType:mimeType fileName:attachmentName]; + } } - - // Add attachment - [mail addAttachmentData:fileData mimeType:mimeType fileName:attachmentName]; } - + UIViewController *root = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; while (root.presentedViewController) { diff --git a/android/src/main/java/com/chirag/RNMail/RNMailModule.java b/android/src/main/java/com/chirag/RNMail/RNMailModule.java index 658ce6a..613e1bc 100644 --- a/android/src/main/java/com/chirag/RNMail/RNMailModule.java +++ b/android/src/main/java/com/chirag/RNMail/RNMailModule.java @@ -66,34 +66,39 @@ public void mail(ReadableMap options, Callback callback) { i.putExtra(Intent.EXTRA_TEXT, Html.fromHtml(body)); } else { i.putExtra(Intent.EXTRA_TEXT, body); - } + } } if (options.hasKey("recipients") && !options.isNull("recipients")) { ReadableArray recipients = options.getArray("recipients"); i.putExtra(Intent.EXTRA_EMAIL, readableArrayToStringArray(recipients)); - } + } if (options.hasKey("ccRecipients") && !options.isNull("ccRecipients")) { ReadableArray ccRecipients = options.getArray("ccRecipients"); i.putExtra(Intent.EXTRA_CC, readableArrayToStringArray(ccRecipients)); } + if (options.hasKey("attachments") && !options.isNull("attachments")) { + ReadableArray r = options.getArray("attachments"); + int length = r.size(); + ArrayList uris = new ArrayList(); + for (int keyIndex = 0; keyIndex < length; keyIndex++) { + ReadableMap clip = r.getMap(keyIndex); + if (clip.hasKey("path") && !clip.isNull("path")){ + String path = clip.getString("path"); + File file = new File(path); + Uri u = Uri.fromFile(file); + uris.add(u); + } + } + i.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + } if (options.hasKey("bccRecipients") && !options.isNull("bccRecipients")) { ReadableArray bccRecipients = options.getArray("bccRecipients"); i.putExtra(Intent.EXTRA_BCC, readableArrayToStringArray(bccRecipients)); } - if (options.hasKey("attachment") && !options.isNull("attachment")) { - ReadableMap attachment = options.getMap("attachment"); - if (attachment.hasKey("path") && !attachment.isNull("path")) { - String path = attachment.getString("path"); - File file = new File(path); - Uri p = Uri.fromFile(file); - i.putExtra(Intent.EXTRA_STREAM, p); - } - } - PackageManager manager = reactContext.getPackageManager(); List list = manager.queryIntentActivities(i, 0); @@ -110,14 +115,14 @@ public void mail(ReadableMap options, Callback callback) { callback.invoke("error"); } } else { - Intent chooser = Intent.createChooser(i, "Send Mail"); - chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Intent chooser = Intent.createChooser(i, "Send Mail"); + chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - try { - reactContext.startActivity(chooser); - } catch (Exception ex) { - callback.invoke("error"); - } + try { + reactContext.startActivity(chooser); + } catch (Exception ex) { + callback.invoke("error"); } } } +} From 8023002ed19cbf35f2af16a1682cadc80bd0807b Mon Sep 17 00:00:00 2001 From: Adam Wilson Date: Wed, 8 Nov 2017 12:52:43 +0000 Subject: [PATCH 2/5] Add iOS audio MIME types --- RNMail/RNMail.m | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/RNMail/RNMail.m b/RNMail/RNMail.m index 1466e4a..ff7cb10 100644 --- a/RNMail/RNMail.m +++ b/RNMail/RNMail.m @@ -113,18 +113,18 @@ + (BOOL)requiresMainQueueSetup mimeType = @"application/zip"; } else if ([attachmentType isEqualToString:@"text"]) { mimeType = @"text/*"; - } else if ([attachmentType isEqualToString:@"mp3"]) { - mimeType = @"audio/mpeg"; - } else if ([attachmentType isEqualToString:@"wav"]) { - mimeType = @"audio/wav"; - } else if ([attachmentType isEqualToString:@"aiff"]) { - mimeType = @"audio/aiff"; - } else if ([attachmentType isEqualToString:@"flac"]) { - mimeType = @"audio/flac"; - } else if ([attachmentType isEqualToString:@"ogg"]) { - mimeType = @"audio/ogg"; - } else if ([attachmentType isEqualToString:@"xls"]) { - mimeType = @"application/vnd.ms-excel"; + } else if ([attachmentType isEqualToString:@"mp3"]) { + mimeType = @"audio/mpeg"; + } else if ([attachmentType isEqualToString:@"wav"]) { + mimeType = @"audio/wav"; + } else if ([attachmentType isEqualToString:@"aiff"]) { + mimeType = @"audio/aiff"; + } else if ([attachmentType isEqualToString:@"flac"]) { + mimeType = @"audio/flac"; + } else if ([attachmentType isEqualToString:@"ogg"]) { + mimeType = @"audio/ogg"; + } else if ([attachmentType isEqualToString:@"xls"]) { + mimeType = @"application/vnd.ms-excel"; } [mail addAttachmentData:fileData mimeType:mimeType fileName:attachmentName]; } From 5606d78aacb1929adc6f684480c20da5a053a3a5 Mon Sep 17 00:00:00 2001 From: Adam Wilson Date: Tue, 15 May 2018 17:59:33 +0100 Subject: [PATCH 3/5] Copy files to temp location before sharing on Android --- .../java/com/chirag/RNMail/RNMailModule.java | 106 +++++++++++++----- 1 file changed, 81 insertions(+), 25 deletions(-) diff --git a/android/src/main/java/com/chirag/RNMail/RNMailModule.java b/android/src/main/java/com/chirag/RNMail/RNMailModule.java index 613e1bc..5f1dac2 100644 --- a/android/src/main/java/com/chirag/RNMail/RNMailModule.java +++ b/android/src/main/java/com/chirag/RNMail/RNMailModule.java @@ -5,16 +5,25 @@ import android.content.pm.ResolveInfo; import android.net.Uri; import android.text.Html; +import android.util.Log; +import com.facebook.common.file.FileUtils; +import com.facebook.react.bridge.Callback; import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; -import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.ReadableArray; -import com.facebook.react.bridge.Callback; +import com.facebook.react.bridge.ReadableMap; -import java.util.List; import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; + /** * NativeModule that allows JS to open emails sending apps chooser. @@ -34,12 +43,11 @@ public String getName() { } /** - * Converts a ReadableArray to a String array - * - * @param r the ReadableArray instance to convert - * - * @return array of strings - */ + * Converts a ReadableArray to a String array + * + * @param r the ReadableArray instance to convert + * @return array of strings + */ private String[] readableArrayToStringArray(ReadableArray r) { int length = r.size(); String[] strArray = new String[length]; @@ -53,8 +61,9 @@ private String[] readableArrayToStringArray(ReadableArray r) { @ReactMethod public void mail(ReadableMap options, Callback callback) { - Intent i = new Intent(Intent.ACTION_SENDTO); - i.setData(Uri.parse("mailto:")); + Intent i = new Intent(Intent.ACTION_SEND_MULTIPLE); + i.setType("message/rfc822"); + if (options.hasKey("subject") && !options.isNull("subject")) { i.putExtra(Intent.EXTRA_SUBJECT, options.getString("subject")); @@ -78,27 +87,55 @@ public void mail(ReadableMap options, Callback callback) { ReadableArray ccRecipients = options.getArray("ccRecipients"); i.putExtra(Intent.EXTRA_CC, readableArrayToStringArray(ccRecipients)); } + + if (options.hasKey("bccRecipients") && !options.isNull("bccRecipients")) { + ReadableArray bccRecipients = options.getArray("bccRecipients"); + i.putExtra(Intent.EXTRA_BCC, readableArrayToStringArray(bccRecipients)); + } + if (options.hasKey("attachments") && !options.isNull("attachments")) { ReadableArray r = options.getArray("attachments"); int length = r.size(); ArrayList uris = new ArrayList(); for (int keyIndex = 0; keyIndex < length; keyIndex++) { ReadableMap clip = r.getMap(keyIndex); - if (clip.hasKey("path") && !clip.isNull("path")){ + if (clip.hasKey("path") && !clip.isNull("path")) { String path = clip.getString("path"); + Log.d ("RNMail", "Attachment file path: " + path); + File file = new File(path); - Uri u = Uri.fromFile(file); - uris.add(u); + + String name, suffix = ""; + if (clip.hasKey("name")) + name = clip.getString("name"); + else + name = file.getName(); + + if (clip.hasKey("type")) + suffix = "." + clip.getString("type"); + + File temporaryFile = null; + try { + temporaryFile = File.createTempFile(name, suffix, reactContext.getExternalCacheDir()); + copy (file, temporaryFile); + } catch (IOException e) { + e.printStackTrace(); + Log.e("RNMail", "Error copying to temporary file"); + } + + temporaryFile.setReadable(true, false); + if (temporaryFile.exists()) { + if (temporaryFile.length() == 0) + Log.d ("RNMail", "Warning, attaching empty file!"); + uris.add(Uri.fromFile(temporaryFile)); + } else { + Log.e("RNMail", "Attachment file does not exist"); + } } } i.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); } - if (options.hasKey("bccRecipients") && !options.isNull("bccRecipients")) { - ReadableArray bccRecipients = options.getArray("bccRecipients"); - i.putExtra(Intent.EXTRA_BCC, readableArrayToStringArray(bccRecipients)); - } - PackageManager manager = reactContext.getPackageManager(); List list = manager.queryIntentActivities(i, 0); @@ -115,14 +152,33 @@ public void mail(ReadableMap options, Callback callback) { callback.invoke("error"); } } else { - Intent chooser = Intent.createChooser(i, "Send Mail"); - chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Intent chooser = Intent.createChooser(i, "Send email..."); + chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + try { + reactContext.startActivity(chooser); + } catch (Exception ex) { + callback.invoke("error"); + } + + } + } + protected static void copy(File src, File dst) throws IOException { + InputStream in = new FileInputStream(src); try { - reactContext.startActivity(chooser); - } catch (Exception ex) { - callback.invoke("error"); + OutputStream out = new FileOutputStream(dst); + try { + // Transfer bytes from in to out + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } finally { + out.close(); + } + } finally { + in.close(); } } } -} From 475bc7570eef60b3ea371be8534a46eca8e92986 Mon Sep 17 00:00:00 2001 From: Adam Wilson Date: Fri, 8 Jun 2018 11:37:11 +0100 Subject: [PATCH 4/5] Switch to FileProvider for file sharing --- .../java/com/chirag/RNMail/RNMailModule.java | 63 ++++++++----------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/android/src/main/java/com/chirag/RNMail/RNMailModule.java b/android/src/main/java/com/chirag/RNMail/RNMailModule.java index 5f1dac2..d5ff434 100644 --- a/android/src/main/java/com/chirag/RNMail/RNMailModule.java +++ b/android/src/main/java/com/chirag/RNMail/RNMailModule.java @@ -4,6 +4,7 @@ import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.net.Uri; +import android.support.v4.content.FileProvider; import android.text.Html; import android.util.Log; @@ -64,7 +65,6 @@ public void mail(ReadableMap options, Callback callback) { Intent i = new Intent(Intent.ACTION_SEND_MULTIPLE); i.setType("message/rfc822"); - if (options.hasKey("subject") && !options.isNull("subject")) { i.putExtra(Intent.EXTRA_SUBJECT, options.getString("subject")); } @@ -94,6 +94,7 @@ public void mail(ReadableMap options, Callback callback) { } if (options.hasKey("attachments") && !options.isNull("attachments")) { + ReadableArray r = options.getArray("attachments"); int length = r.size(); ArrayList uris = new ArrayList(); @@ -114,26 +115,35 @@ public void mail(ReadableMap options, Callback callback) { if (clip.hasKey("type")) suffix = "." + clip.getString("type"); - File temporaryFile = null; - try { - temporaryFile = File.createTempFile(name, suffix, reactContext.getExternalCacheDir()); - copy (file, temporaryFile); - } catch (IOException e) { - e.printStackTrace(); - Log.e("RNMail", "Error copying to temporary file"); - } - temporaryFile.setReadable(true, false); - if (temporaryFile.exists()) { - if (temporaryFile.length() == 0) + file.setReadable(true, false); + if (file.exists()) { + + if (file.length() == 0) Log.d ("RNMail", "Warning, attaching empty file!"); - uris.add(Uri.fromFile(temporaryFile)); + // Use the FileProvider to get a content URI + try { + Uri fileUri = FileProvider.getUriForFile( + getCurrentActivity(), + reactContext.getPackageName() + ".fileprovider", + file); + if (fileUri != null) { + // Grant temporary read permission to the content URI + uris.add(fileUri); + } + } catch (Exception e) { + String message = "There was a problem sharing the file " + file.getName(); + Log.e("RNMail", message); + callback.invoke("error", message + "\n" + e.getMessage()); + } } else { Log.e("RNMail", "Attachment file does not exist"); } } } + i.setType("*/*"); i.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); } PackageManager manager = reactContext.getPackageManager(); @@ -148,37 +158,18 @@ public void mail(ReadableMap options, Callback callback) { i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { reactContext.startActivity(i); - } catch (Exception ex) { - callback.invoke("error"); + } catch (Exception e) { + callback.invoke("error", e.getMessage()); } } else { Intent chooser = Intent.createChooser(i, "Send email..."); chooser.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { reactContext.startActivity(chooser); - } catch (Exception ex) { - callback.invoke("error"); + } catch (Exception e) { + callback.invoke("error", e.getMessage()); } } } - - protected static void copy(File src, File dst) throws IOException { - InputStream in = new FileInputStream(src); - try { - OutputStream out = new FileOutputStream(dst); - try { - // Transfer bytes from in to out - byte[] buf = new byte[1024]; - int len; - while ((len = in.read(buf)) > 0) { - out.write(buf, 0, len); - } - } finally { - out.close(); - } - } finally { - in.close(); - } - } } From d252f93d23f396318991603bf21a46e137472a53 Mon Sep 17 00:00:00 2001 From: Adam Wilson Date: Sat, 9 Jun 2018 08:28:55 +0100 Subject: [PATCH 5/5] Update with FileProvider info and Android changes --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3bf1f4c..023ed23 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,11 @@ public class MainApplication extends Application implements ReactApplication { ``` +#### File Attachments on Android + +Addition setup is required for Android file sharing, see https://developer.android.com/training/secure-file-sharing/setup-sharing + +It is expected that the `android:authorities` property is set to the app's package name with ".fileprovider" appended to the end. ### Manual Installation: iOS @@ -162,7 +167,7 @@ export default class App extends Component { ### Note -On Android, the `callback` will only be called if an `error` occurs. The `event` argument is unused! +On Android, the `callback` will only be called if an `error` occurs. The `event` argument will contain a string with the exception or error message. ## Here is how it looks: ![Demo gif](https://github.com/chirag04/react-native-mail/blob/master/screenshot.png)