From edc0147868303142319f89cc752a4fc3f915191c Mon Sep 17 00:00:00 2001 From: M66B Date: Wed, 13 Mar 2024 13:31:55 +0100 Subject: [PATCH] Added send partial --- .../java/eu/faircode/email/DaoMessage.java | 14 ++++ .../java/eu/faircode/email/EmailService.java | 4 ++ .../eu/faircode/email/FragmentMessages.java | 20 ++++-- .../faircode/email/FragmentOptionsSend.java | 13 +++- .../java/eu/faircode/email/MessageHelper.java | 19 ++++++ .../java/eu/faircode/email/ServiceSend.java | 67 ++++++++++++++++--- .../main/res/layout/fragment_options_send.xml | 22 ++++++ app/src/main/res/values/strings.xml | 3 + 8 files changed, 148 insertions(+), 14 deletions(-) diff --git a/app/src/main/java/eu/faircode/email/DaoMessage.java b/app/src/main/java/eu/faircode/email/DaoMessage.java index 4b9a545f6b..a69530b228 100644 --- a/app/src/main/java/eu/faircode/email/DaoMessage.java +++ b/app/src/main/java/eu/faircode/email/DaoMessage.java @@ -32,6 +32,8 @@ import androidx.room.Update; import java.util.List; +import javax.mail.Address; + @Dao public interface DaoMessage { @@ -761,6 +763,15 @@ public interface DaoMessage { @Query("UPDATE message SET fts = :fts WHERE id = :id AND NOT (fts IS :fts)") int setMessageFts(long id, boolean fts); + @Query("UPDATE message SET `to` = :to WHERE id = :id AND NOT (`to` IS :to)") + int setMessageTo(long id, String to); + + @Query("UPDATE message SET `cc` = :cc WHERE id = :id AND NOT (`cc` IS :cc)") + int setMessageCc(long id, String cc); + + @Query("UPDATE message SET `bcc` = :bcc WHERE id = :id AND NOT (`bcc` IS :bcc)") + int setMessageBcc(long id, String bcc); + @Query("UPDATE message SET received = :received WHERE id = :id AND NOT (received IS :received)") int setMessageReceived(long id, long received); @@ -837,6 +848,9 @@ public interface DaoMessage { " AND (NOT (received IS :sent) OR NOT (sent IS :sent))") int setMessageSent(long id, Long sent); + @Query("UPDATE message SET warning = :warning WHERE id = :id AND NOT (warning IS :warning)") + int setMessageWarning(long id, String warning); + @Query("UPDATE message SET error = :error WHERE id = :id AND NOT (error IS :error)") int setMessageError(long id, String error); diff --git a/app/src/main/java/eu/faircode/email/EmailService.java b/app/src/main/java/eu/faircode/email/EmailService.java index 050b0c628c..b3194fab72 100644 --- a/app/src/main/java/eu/faircode/email/EmailService.java +++ b/app/src/main/java/eu/faircode/email/EmailService.java @@ -332,6 +332,10 @@ public class EmailService implements AutoCloseable { properties.put("mail." + protocol + ".ignorebodystructuresize", Boolean.toString(enabled)); } + void setSendPartial(boolean enabled) { + properties.put("mail." + protocol + ".sendpartial", Boolean.toString(enabled)); + } + void setUseIp(boolean enabled, String host) { this.useip = enabled; this.ehlo = host; diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index 6be8bd0169..bce4205fed 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -3040,10 +3040,17 @@ public class FragmentMessages extends FragmentBase swipes.left_type = null; } else if (EntityFolder.OUTBOX.equals(message.folderType)) { swipes = new TupleAccountSwipes(); - swipes.swipe_right = 0L; - swipes.right_type = EntityFolder.DRAFTS; - swipes.swipe_left = 0L; - swipes.left_type = EntityFolder.DRAFTS; + if (message.warning == null) { + swipes.swipe_right = 0L; + swipes.right_type = EntityFolder.DRAFTS; + swipes.swipe_left = 0L; + swipes.left_type = EntityFolder.DRAFTS; + } else { + swipes.swipe_right = EntityMessage.SWIPE_ACTION_DELETE; + swipes.right_type = null; + swipes.swipe_left = EntityMessage.SWIPE_ACTION_DELETE; + swipes.left_type = null; + } } else { swipes = accountSwipes.get(message.account); if (swipes == null) @@ -3200,7 +3207,10 @@ public class FragmentMessages extends FragmentBase } if (EntityFolder.OUTBOX.equals(message.folderType)) { - ActivityCompose.undoSend(message.id, getContext(), getViewLifecycleOwner(), getParentFragmentManager()); + if (message.warning == null) + ActivityCompose.undoSend(message.id, getContext(), getViewLifecycleOwner(), getParentFragmentManager()); + else + onDelete(message.id); return; } diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsSend.java b/app/src/main/java/eu/faircode/email/FragmentOptionsSend.java index 440eb676d7..658f6e34c1 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsSend.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsSend.java @@ -128,6 +128,7 @@ public class FragmentOptionsSend extends FragmentBase implements SharedPreferenc private SwitchCompat swReplyMove; private SwitchCompat swReplyMoveInbox; private EditText etSendRetryMax; + private SwitchCompat swSendPartial; private final static List RESET_OPTIONS = Collections.unmodifiableList(Arrays.asList( "keyboard", "keyboard_no_fullscreen", @@ -148,7 +149,7 @@ public class FragmentOptionsSend extends FragmentBase implements SharedPreferenc "receipt_default", "receipt_type", "receipt_legacy", "forward_new", "lookup_mx", "reply_move", "reply_move_inbox", - "send_retry_max" + "send_retry_max", "send_partial" )); @Override @@ -222,6 +223,7 @@ public class FragmentOptionsSend extends FragmentBase implements SharedPreferenc swReplyMove = view.findViewById(R.id.swReplyMove); swReplyMoveInbox = view.findViewById(R.id.swReplyMoveInbox); etSendRetryMax = view.findViewById(R.id.etSendRetryMax); + swSendPartial = view.findViewById(R.id.swSendPartial); List fonts = StyleHelper.getFonts(getContext(), false); @@ -786,6 +788,13 @@ public class FragmentOptionsSend extends FragmentBase implements SharedPreferenc } }); + swSendPartial.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { + @Override + public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { + prefs.edit().putBoolean("send_partial", checked).apply(); + } + }); + // Initialize FragmentDialogTheme.setBackground(getContext(), view, false); @@ -968,6 +977,8 @@ public class FragmentOptionsSend extends FragmentBase implements SharedPreferenc int send_retry_max = prefs.getInt("send_retry_max", 0); etSendRetryMax.setText(send_retry_max > 0 ? Integer.toString(send_retry_max) : null); etSendRetryMax.setHint(Integer.toString(ServiceSend.RETRY_MAX_DEFAULT)); + + swSendPartial.setChecked(prefs.getBoolean("send_partial", false)); } catch (Throwable ex) { Log.e(ex); } diff --git a/app/src/main/java/eu/faircode/email/MessageHelper.java b/app/src/main/java/eu/faircode/email/MessageHelper.java index 37607a6076..1dcaf60ed9 100644 --- a/app/src/main/java/eu/faircode/email/MessageHelper.java +++ b/app/src/main/java/eu/faircode/email/MessageHelper.java @@ -5677,6 +5677,25 @@ public class MessageHelper { return false; } + static Address[] removeAddresses(Address[] addresses, List
removes) { + if (addresses == null || addresses.length == 0) + return new Address[0]; + + List
result = new ArrayList<>(); + for (Address address : addresses) { + boolean found = false; + for (Address remove : removes) + if (equalEmail(address, remove)) { + found = true; + break; + } + if (!found) + result.add(address); + } + + return result.toArray(new Address[0]); + } + static boolean equalEmail(Address a1, Address a2) { String email1 = ((InternetAddress) a1).getAddress(); String email2 = ((InternetAddress) a2).getAddress(); diff --git a/app/src/main/java/eu/faircode/email/ServiceSend.java b/app/src/main/java/eu/faircode/email/ServiceSend.java index e5eaf3957e..e973392e4f 100644 --- a/app/src/main/java/eu/faircode/email/ServiceSend.java +++ b/app/src/main/java/eu/faircode/email/ServiceSend.java @@ -294,7 +294,7 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar .setSmallIcon(R.drawable.baseline_warning_white_24) .setContentTitle(getString(R.string.title_notification_sending_failed, recipient)) .setContentIntent(getPendingIntent(this)) - .setAutoCancel(tries_left != 0) + .setAutoCancel(true) .setShowWhen(true) .setPriority(NotificationCompat.PRIORITY_MAX) .setOnlyAlertOnce(false) @@ -573,7 +573,7 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar // Requeue non executing operations for (long id : db.message().getMessageByFolder(outbox.id)) { EntityMessage message = db.message().getMessage(id); - if (message == null) + if (message == null || message.warning != null) continue; EntityOperation op = db.operation().getOperation(message.id, EntityOperation.SEND); @@ -623,6 +623,7 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar nm.notify(NotificationHelper.NOTIFICATION_SEND, getNotificationService(true)); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + boolean send_partial = prefs.getBoolean("send_partial", false); boolean reply_move = prefs.getBoolean("reply_move", false); boolean reply_move_inbox = prefs.getBoolean("reply_move_inbox", true); boolean protocol = prefs.getBoolean("protocol", false); @@ -761,8 +762,10 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar } // Create transport - long start, end; + long start = 0; + long end = 0; Long max_size = null; + SMTPSendFailedException partial = null; if (ident.auth_type == AUTH_TYPE_GRAPH) { start = new Date().getTime(); MicrosoftGraph.send(ServiceSend.this, ident, imessage); @@ -770,6 +773,8 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar } else { EmailService iservice = new EmailService(this, ident, EmailService.PURPOSE_USE, debug); try { + if (send_partial) + iservice.setSendPartial(true); iservice.setUseIp(ident.use_ip, ident.ehlo); if (!message.isSigned() && !message.isEncrypted()) iservice.set8BitMime(ident.octetmime); @@ -820,6 +825,12 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar recipients.add(a); } + if (BuildConfig.DEBUG && false) { + InternetAddress invalid = new InternetAddress(); + invalid.setAddress("invalid"); + recipients.add(invalid); + } + if (protocol && BuildConfig.DEBUG) { ByteArrayOutputStream bos = new ByteArrayOutputStream(); imessage.writeTo(bos); @@ -855,15 +866,27 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar // Send message EntityLog.log(this, "Sending " + via); start = new Date().getTime(); - iservice.getTransport().sendMessage(imessage, rcptto); - end = new Date().getTime(); + try { + iservice.getTransport().sendMessage(imessage, rcptto); + } finally { + end = new Date().getTime(); + } EntityLog.log(this, "Sent " + via + " elapse=" + (end - start) + " ms"); } catch (MessagingException ex) { + iservice.dump(ident.email); Log.e(ex); if (ex instanceof SMTPSendFailedException) { SMTPSendFailedException sem = (SMTPSendFailedException) ex; + if (send_partial && + sem.getInvalidAddresses() != null && + sem.getValidSentAddresses() != null && + sem.getValidUnsentAddresses() != null && + sem.getValidSentAddresses().length > 0 && + sem.getInvalidAddresses().length + sem.getValidUnsentAddresses().length > 0) { + partial = sem; + } ex = new SMTPSendFailedException( sem.getCommand(), sem.getReturnCode(), @@ -874,12 +897,13 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar sem.getInvalidAddresses()); } - if (sid != null) + if (sid != null && partial == null) db.message().deleteMessage(sid); db.identity().setIdentityError(ident.id, Log.formatThrowable(ex)); - throw ex; + if (partial == null) + throw ex; } catch (Throwable ex) { iservice.dump(ident.email); throw ex; @@ -898,7 +922,20 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar db.beginTransaction(); // Delete from outbox - db.message().deleteMessage(message.id); + if (partial == null) + db.message().deleteMessage(message.id); + else { + Throwable ex = new Throwable(getString(R.string.title_advanced_sent_partially), partial); + db.message().setMessageWarning(message.id, Log.formatThrowable(ex)); + if (NotificationHelper.areNotificationsEnabled(nm)) { + NotificationCompat.Builder builder = getNotificationError( + MessageHelper.formatAddressesShort(message.to), + ex, 0); + nm.notify("partial:" + message.id, + NotificationHelper.NOTIFICATION_TAGGED, + builder.build()); + } + } // Show in sent folder if (sid != null) { @@ -907,6 +944,20 @@ public class ServiceSend extends ServiceBase implements SharedPreferences.OnShar db.attachment().deleteAttachments(sid, new int[]{EntityAttachment.PGP_MESSAGE, EntityAttachment.SMIME_MESSAGE}); + if (partial != null) { + List
unsent = new ArrayList<>(); + if (partial.getInvalidAddresses() != null) + unsent.addAll(Arrays.asList(partial.getInvalidAddresses())); + if (partial.getValidUnsentAddresses() != null) + unsent.addAll(Arrays.asList(partial.getValidUnsentAddresses())); + db.message().setMessageTo(sid, + DB.Converters.encodeAddresses(MessageHelper.removeAddresses(message.to, unsent))); + db.message().setMessageCc(sid, + DB.Converters.encodeAddresses(MessageHelper.removeAddresses(message.cc, unsent))); + db.message().setMessageBcc(sid, + DB.Converters.encodeAddresses(MessageHelper.removeAddresses(message.bcc, unsent))); + } + db.message().setMessageReceived(sid, start); db.message().setMessageSent(sid, end); db.message().setMessageUiHide(sid, false); diff --git a/app/src/main/res/layout/fragment_options_send.xml b/app/src/main/res/layout/fragment_options_send.xml index 4c96de1fb5..db180de15d 100644 --- a/app/src/main/res/layout/fragment_options_send.xml +++ b/app/src/main/res/layout/fragment_options_send.xml @@ -1158,6 +1158,28 @@ android:textStyle="italic" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/etSendRetryMax" /> + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5d92ce75d1..28a4f70fef 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -501,6 +501,9 @@ Also for messages in the inbox Maximum send attempts Sending will be retried on connectivity changes + Allow sending to some recipients + If some addresses are invalid, a message will still be sent to the other addresses + The message was not sent to all recipients Automatically create links Send plain text only by default