diff --git a/app/src/main/java/eu/faircode/email/DaoAttachment.java b/app/src/main/java/eu/faircode/email/DaoAttachment.java index c31326cc74..4f8f5136be 100644 --- a/app/src/main/java/eu/faircode/email/DaoAttachment.java +++ b/app/src/main/java/eu/faircode/email/DaoAttachment.java @@ -30,17 +30,33 @@ import androidx.room.Update; @Dao public interface DaoAttachment { - @Query("SELECT id,message,sequence,name,type,size,progress" + + @Query("SELECT id, message, sequence, name, type, size, progress" + ", (NOT content IS NULL) as content" + - " FROM attachment WHERE message = :message") + " FROM attachment" + + " WHERE message = :message" + + " ORDER BY sequence") LiveData> liveAttachments(long message); - @Query("SELECT * FROM attachment WHERE message = :message AND sequence = :sequence") + @Query("SELECT * FROM attachment" + + " WHERE message = :message" + + " AND sequence = :sequence") EntityAttachment getAttachment(long message, int sequence); - @Query("SELECT COUNT(attachment.id) FROM attachment WHERE message = :message") + @Query("SELECT COUNT(attachment.id)" + + " FROM attachment" + + " WHERE message = :message") int getAttachmentCount(long message); + @Query("SELECT SUM(CASE WHEN content IS NULL THEN 1 ELSE 0 END)" + + " FROM attachment" + + " WHERE message = :message") + int getAttachmentCountWithoutContent(long message); + + @Query("SELECT id, message, sequence, name, type, size, progress, NULL AS content FROM attachment" + + " WHERE message = :message" + + " ORDER BY sequence") + List getAttachments(long message); + @Query("UPDATE attachment SET progress = :progress WHERE id = :id") void setProgress(long id, int progress); diff --git a/app/src/main/java/eu/faircode/email/FragmentCompose.java b/app/src/main/java/eu/faircode/email/FragmentCompose.java index 1852ac9910..f1c7ee0a1a 100644 --- a/app/src/main/java/eu/faircode/email/FragmentCompose.java +++ b/app/src/main/java/eu/faircode/email/FragmentCompose.java @@ -99,6 +99,8 @@ public class FragmentCompose extends FragmentEx { private AdapterAttachment adapter; + private static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes + @Override @Nullable public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { @@ -417,7 +419,7 @@ public class FragmentCompose extends FragmentEx { attachment.size = (size == null ? null : Integer.parseInt(size)); attachment.progress = 0; - db.attachment().insertAttachment(attachment); + attachment.id = db.attachment().insertAttachment(attachment); InputStream is = null; try { @@ -425,7 +427,7 @@ public class FragmentCompose extends FragmentEx { ByteArrayOutputStream os = new ByteArrayOutputStream(); int len; - byte[] buffer = new byte[8192]; + byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE]; while ((len = is.read(buffer)) > 0) { os.write(buffer, 0, len); @@ -445,7 +447,6 @@ public class FragmentCompose extends FragmentEx { is.close(); } - return null; } finally { if (cursor != null) @@ -728,6 +729,13 @@ public class FragmentCompose extends FragmentEx { if (draft.to == null && draft.cc == null && draft.bcc == null) throw new IllegalArgumentException(getContext().getString(R.string.title_to_missing)); + if (db.attachment().getAttachmentCountWithoutContent(draft.id) > 0) + throw new IllegalArgumentException(getContext().getString(R.string.title_attachments_missing)); + + List attachments = db.attachment().getAttachments(draft.id); + for (EntityAttachment attachment : attachments) + attachment.content = db.attachment().getContent(attachment.id); + // Delete draft (cannot move to outbox) draft.ui_hide = true; db.message().updateMessage(draft); @@ -740,6 +748,11 @@ public class FragmentCompose extends FragmentEx { draft.ui_hide = false; draft.id = db.message().insertMessage(draft); + for (EntityAttachment attachment : attachments) { + attachment.message = draft.id; + db.attachment().insertAttachment(attachment); + } + EntityOperation.queue(db, draft, EntityOperation.SEND); } diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java index 20a7f99be0..62e4500dcb 100644 --- a/app/src/main/java/eu/faircode/email/MessageHelper.java +++ b/app/src/main/java/eu/faircode/email/MessageHelper.java @@ -27,11 +27,14 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Properties; +import javax.activation.DataHandler; +import javax.activation.DataSource; import javax.mail.Address; import javax.mail.BodyPart; import javax.mail.Flags; @@ -42,7 +45,10 @@ import javax.mail.Part; import javax.mail.Session; import javax.mail.internet.ContentType; import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeBodyPart; import javax.mail.internet.MimeMessage; +import javax.mail.internet.MimeMultipart; +import javax.mail.util.ByteArrayDataSource; public class MessageHelper { private MimeMessage imessage; @@ -78,7 +84,7 @@ public class MessageHelper { return props; } - static MimeMessageEx from(EntityMessage message, Session isession) throws MessagingException { + static MimeMessageEx from(EntityMessage message, List attachments, Session isession) throws MessagingException { MimeMessageEx imessage = new MimeMessageEx(isession, message.id); if (message.from != null && message.from.length > 0) @@ -96,16 +102,38 @@ public class MessageHelper { if (message.subject != null) imessage.setSubject(message.subject); - if (message.body != null) - imessage.setText(message.body, null, "html"); + if (attachments.size() == 0) { + if (message.body != null) + imessage.setText(message.body, Charset.defaultCharset().name(), "html"); + } else { + Multipart multipart = new MimeMultipart(); + + if (message.body != null) { + BodyPart bpMessage = new MimeBodyPart(); + bpMessage.setContent(message.body, "text/html; charset=" + Charset.defaultCharset().name()); + multipart.addBodyPart(bpMessage); + } + + for (EntityAttachment attachment : attachments) { + BodyPart bpAttachment = new MimeBodyPart(); + bpAttachment.setFileName(attachment.name); + + DataSource dataSource = new ByteArrayDataSource(attachment.content, attachment.type); + bpAttachment.setDataHandler(new DataHandler(dataSource)); + + multipart.addBodyPart(bpAttachment); + } + + imessage.setContent(multipart); + } imessage.setSentDate(new Date()); return imessage; } - static MimeMessageEx from(EntityMessage message, EntityMessage reply, Session isession) throws MessagingException { - MimeMessageEx imessage = from(message, isession); + static MimeMessageEx from(EntityMessage message, EntityMessage reply, List attachments, Session isession) throws MessagingException { + MimeMessageEx imessage = from(message, attachments, isession); imessage.addHeader("In-Reply-To", reply.msgid); imessage.addHeader("References", (reply.references == null ? "" : reply.references + " ") + reply.msgid); return imessage; diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java index fdd8eeb232..60a43bc61c 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java +++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java @@ -103,7 +103,7 @@ public class ServiceSynchronize extends LifecycleService { private static final long NOOP_INTERVAL = 9 * 60 * 1000L; // ms private static final int FETCH_BATCH_SIZE = 10; - private static final int DOWNLOAD_BUFFER_SIZE = 8192; // bytes + private static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes static final String ACTION_PROCESS_FOLDER = BuildConfig.APPLICATION_ID + ".PROCESS_FOLDER"; static final String ACTION_PROCESS_OUTBOX = BuildConfig.APPLICATION_ID + ".PROCESS_OUTBOX"; @@ -705,10 +705,12 @@ public class ServiceSynchronize extends LifecycleService { if (EntityOperation.SEEN.equals(op.name)) doSeen(folder, ifolder, jargs, message); - else if (EntityOperation.ADD.equals(op.name)) - doAdd(folder, ifolder, message); - - else if (EntityOperation.MOVE.equals(op.name)) + else if (EntityOperation.ADD.equals(op.name)) { + List attachments = db.attachment().getAttachments(message.id); + for (EntityAttachment attachment : attachments) + attachment.content = db.attachment().getContent(attachment.id); + doAdd(folder, ifolder, message, attachments); + } else if (EntityOperation.MOVE.equals(op.name)) doMove(folder, istore, ifolder, db, jargs, message); else if (EntityOperation.DELETE.equals(op.name)) @@ -774,11 +776,11 @@ public class ServiceSynchronize extends LifecycleService { imessage.setFlag(Flags.Flag.SEEN, jargs.getBoolean(0)); } - private void doAdd(EntityFolder folder, IMAPFolder ifolder, EntityMessage message) throws MessagingException { + private void doAdd(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, List attachments) throws MessagingException { // Append message Properties props = MessageHelper.getSessionProperties(); Session isession = Session.getInstance(props, null); - MimeMessage imessage = MessageHelper.from(message, isession); + MimeMessage imessage = MessageHelper.from(message, attachments, isession); ifolder.appendMessages(new Message[]{imessage}); } @@ -825,9 +827,13 @@ public class ServiceSynchronize extends LifecycleService { // Append copy if (!EntityFolder.ARCHIVE.equals(target.type)) { + List attachments = db.attachment().getAttachments(message.id); + for (EntityAttachment attachment : attachments) + attachment.content = db.attachment().getContent(attachment.id); + Properties props = MessageHelper.getSessionProperties(); Session isession = Session.getInstance(props, null); - MimeMessage icopy = MessageHelper.from(message, isession); + MimeMessage icopy = MessageHelper.from(message, attachments, isession); itarget.appendMessages(new Message[]{icopy}); } @@ -856,51 +862,54 @@ public class ServiceSynchronize extends LifecycleService { private void doSend(DB db, EntityMessage message) throws MessagingException { // Send message - EntityMessage reply = (message.replying == null ? null : db.message().getMessage(message.replying)); EntityIdentity ident = db.identity().getIdentity(message.identity); + EntityMessage reply = (message.replying == null ? null : db.message().getMessage(message.replying)); + List attachments = db.attachment().getAttachments(message.id); + for (EntityAttachment attachment : attachments) + attachment.content = db.attachment().getContent(attachment.id); if (!ident.synchronize) { // Message will remain in outbox return; } - try { - db.beginTransaction(); + // Create session + Properties props = MessageHelper.getSessionProperties(); + Session isession = Session.getInstance(props, null); - // Move message to sent - EntityFolder sent = db.folder().getFolderByType(ident.account, EntityFolder.SENT); - if (sent == null) - ; // Leave message in outbox - else { - message.folder = sent.id; - message.uid = null; - } + // Create message + MimeMessage imessage; + if (reply == null) + imessage = MessageHelper.from(message, attachments, isession); + else + imessage = MessageHelper.from(message, reply, attachments, isession); + if (ident.replyto != null) + imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)}); - // Create session - Properties props = MessageHelper.getSessionProperties(); - Session isession = Session.getInstance(props, null); + // Create transport + // TODO: cache transport? + Transport itransport = isession.getTransport(ident.starttls ? "smtp" : "smtps"); + try { + // Connect transport + itransport.connect(ident.host, ident.port, ident.user, ident.password); - // Create message - MimeMessage imessage; - if (reply == null) - imessage = MessageHelper.from(message, isession); - else - imessage = MessageHelper.from(message, reply, isession); - if (ident.replyto != null) - imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)}); - - // Create transport - // TODO: cache transport? - Transport itransport = isession.getTransport(ident.starttls ? "smtp" : "smtps"); - try { - // Connect transport - itransport.connect(ident.host, ident.port, ident.user, ident.password); + // Send message + Address[] to = imessage.getAllRecipients(); + itransport.sendMessage(imessage, to); + Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user + + " to " + TextUtils.join(", ", to)); - // Send message - Address[] to = imessage.getAllRecipients(); - itransport.sendMessage(imessage, to); - Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user + - " to " + TextUtils.join(", ", to)); + try { + db.beginTransaction(); + + // Move message to sent + EntityFolder sent = db.folder().getFolderByType(ident.account, EntityFolder.SENT); + if (sent == null) + ; // Leave message in outbox + else { + message.folder = sent.id; + message.uid = null; + } // Update state if (message.thread == null) @@ -913,13 +922,12 @@ public class ServiceSynchronize extends LifecycleService { if (sent != null) EntityOperation.queue(db, message, EntityOperation.ADD); // Could already exist + db.setTransactionSuccessful(); } finally { - itransport.close(); + db.endTransaction(); } - - db.setTransactionSuccessful(); } finally { - db.endTransaction(); + itransport.close(); } } @@ -943,7 +951,7 @@ public class ServiceSynchronize extends LifecycleService { // Download attachment InputStream is = a.part.getInputStream(); ByteArrayOutputStream os = new ByteArrayOutputStream(); - byte[] buffer = new byte[DOWNLOAD_BUFFER_SIZE]; + byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE]; for (int len = is.read(buffer); len != -1; len = is.read(buffer)) { os.write(buffer, 0, len); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81df3faff7..a66c8a0b5b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -141,6 +141,7 @@ Sender missing Recipient missing + Attachments still loading Draft trashed Draft saved Sending message