diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 27b1528399..3a590494f7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -162,6 +162,8 @@
+
+
diff --git a/app/src/main/java/eu/faircode/email/EntityOperation.java b/app/src/main/java/eu/faircode/email/EntityOperation.java
index 1ab4d243e9..39050a6ee0 100644
--- a/app/src/main/java/eu/faircode/email/EntityOperation.java
+++ b/app/src/main/java/eu/faircode/email/EntityOperation.java
@@ -20,6 +20,7 @@ package eu.faircode.email;
*/
import android.content.Context;
+import android.content.Intent;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
@@ -221,6 +222,9 @@ public class EntityOperation {
} else if (DELETE.equals(name))
db.message().setMessageUiHide(message.id, true);
+
+ else if (SEND.equals(name))
+ context.startService(new Intent(context, ServiceSend.class));
} catch (JSONException ex) {
Log.e(ex);
}
diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java
index 75c1f5ea0d..5f8dbd35dc 100644
--- a/app/src/main/java/eu/faircode/email/Helper.java
+++ b/app/src/main/java/eu/faircode/email/Helper.java
@@ -21,6 +21,8 @@ package eu.faircode.email;
import android.accounts.Account;
import android.accounts.AccountManager;
+import android.app.Notification;
+import android.app.PendingIntent;
import android.app.usage.UsageStatsManager;
import android.content.ActivityNotFoundException;
import android.content.Context;
@@ -100,6 +102,7 @@ import javax.net.ssl.SSLException;
import androidx.annotation.NonNull;
import androidx.browser.customtabs.CustomTabsIntent;
+import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner;
@@ -109,7 +112,8 @@ import static androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_C
public class Helper {
static final int NOTIFICATION_SYNCHRONIZE = 1;
- static final int NOTIFICATION_EXTERNAL = 2;
+ static final int NOTIFICATION_SEND = 2;
+ static final int NOTIFICATION_EXTERNAL = 3;
static final int JOB_DAILY = 1001;
@@ -1035,4 +1039,38 @@ public class Helper {
static String sanitizeFilename(String name) {
return (name == null ? null : name.replaceAll("[^a-zA-Z0-9\\.\\-]", "_"));
}
+
+ static NotificationCompat.Builder getNotificationError(Context context, String title, Throwable ex) {
+ return getNotificationError(context, "error", title, ex, true);
+ }
+
+ static NotificationCompat.Builder getNotificationError(Context context, String channel, String title, Throwable ex, boolean debug) {
+ // Build pending intent
+ Intent intent = new Intent(context, ActivitySetup.class);
+ if (debug)
+ intent.setAction("error");
+ intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ PendingIntent pi = PendingIntent.getActivity(
+ context, ActivitySetup.REQUEST_ERROR, intent, PendingIntent.FLAG_UPDATE_CURRENT);
+
+ // Build notification
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channel);
+
+ builder
+ .setSmallIcon(R.drawable.baseline_warning_white_24)
+ .setContentTitle(context.getString(R.string.title_notification_failed, title))
+ .setContentText(Helper.formatThrowable(ex))
+ .setContentIntent(pi)
+ .setAutoCancel(false)
+ .setShowWhen(true)
+ .setPriority(Notification.PRIORITY_MAX)
+ .setOnlyAlertOnce(true)
+ .setCategory(Notification.CATEGORY_ERROR)
+ .setVisibility(NotificationCompat.VISIBILITY_SECRET);
+
+ builder.setStyle(new NotificationCompat.BigTextStyle()
+ .bigText(Helper.formatThrowable(ex, false, "\n")));
+
+ return builder;
+ }
}
diff --git a/app/src/main/java/eu/faircode/email/ReceiverAutostart.java b/app/src/main/java/eu/faircode/email/ReceiverAutostart.java
index 0cb690a4fa..5d89fb5f2e 100644
--- a/app/src/main/java/eu/faircode/email/ReceiverAutostart.java
+++ b/app/src/main/java/eu/faircode/email/ReceiverAutostart.java
@@ -33,6 +33,7 @@ public class ReceiverAutostart extends BroadcastReceiver {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) ||
Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) {
EntityLog.log(context, intent.getAction());
+
ServiceSynchronize.init(context, true);
Thread thread = new Thread(new Runnable() {
@@ -40,9 +41,18 @@ public class ReceiverAutostart extends BroadcastReceiver {
public void run() {
try {
DB db = DB.getInstance(context);
+
List messages = db.message().getSnoozed();
for (EntityMessage message : messages)
EntityMessage.snooze(context, message.id, message.ui_snoozed);
+
+ EntityFolder outbox = db.folder().getOutbox();
+ if (outbox == null)
+ return;
+
+ if (db.operation().getOperations(outbox.id).size() > 0)
+ context.startService(new Intent(context, ServiceSend.class));
+
} catch (Throwable ex) {
Log.e(ex);
}
diff --git a/app/src/main/java/eu/faircode/email/ServiceSend.java b/app/src/main/java/eu/faircode/email/ServiceSend.java
new file mode 100644
index 0000000000..ba7b8ca7b2
--- /dev/null
+++ b/app/src/main/java/eu/faircode/email/ServiceSend.java
@@ -0,0 +1,378 @@
+package eu.faircode.email;
+
+/*
+ This file is part of FairEmail.
+
+ FairEmail is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ FairEmail is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with FairEmail. If not, see .
+
+ Copyright 2018-2019 by Marcel Bokhorst (M66B)
+*/
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.content.Context;
+import android.content.Intent;
+import android.net.ConnectivityManager;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkInfo;
+import android.net.NetworkRequest;
+import android.os.PowerManager;
+import android.text.TextUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+import java.util.Properties;
+
+import javax.mail.Address;
+import javax.mail.AuthenticationFailedException;
+import javax.mail.Message;
+import javax.mail.MessageRemovedException;
+import javax.mail.MessagingException;
+import javax.mail.SendFailedException;
+import javax.mail.Session;
+import javax.mail.Transport;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+
+import androidx.core.app.NotificationCompat;
+import androidx.lifecycle.LifecycleService;
+
+public class ServiceSend extends LifecycleService {
+ private static final int IDENTITY_ERROR_AFTER = 30; // minutes
+
+ @Override
+ public void onCreate() {
+ Log.i("Service send create");
+ super.onCreate();
+
+ ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+ NetworkRequest.Builder builder = new NetworkRequest.Builder();
+ builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
+ cm.registerNetworkCallback(builder.build(), networkCallback);
+ }
+
+ @Override
+ public void onDestroy() {
+ Log.i("Service send destroy");
+
+ ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+ cm.unregisterNetworkCallback(networkCallback);
+
+ stopForeground(true);
+
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancel(Helper.NOTIFICATION_SEND);
+
+ super.onDestroy();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ // Build notification
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "service");
+
+ builder
+ .setSmallIcon(R.drawable.baseline_send_24)
+ .setContentTitle(getString(R.string.title_notification_sending))
+ .setAutoCancel(false)
+ .setShowWhen(false)
+ .setPriority(Notification.PRIORITY_MIN)
+ .setCategory(Notification.CATEGORY_STATUS)
+ .setVisibility(NotificationCompat.VISIBILITY_SECRET);
+
+ startForeground(Helper.NOTIFICATION_SEND, builder.build());
+
+ super.onStartCommand(intent, flags, startId);
+
+ return START_STICKY;
+ }
+
+ ConnectivityManager.NetworkCallback networkCallback = new ConnectivityManager.NetworkCallback() {
+ private Thread thread = null;
+
+ @Override
+ public void onAvailable(Network network) {
+ Log.i("Service send available=" + network);
+
+ if (thread == null || !thread.isAlive()) {
+ thread = new Thread(new Runnable() {
+ @Override
+ public void run() {
+ PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ PowerManager.WakeLock wl = pm.newWakeLock(
+ PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":send");
+ try {
+ wl.acquire();
+
+ DB db = DB.getInstance(ServiceSend.this);
+ EntityFolder outbox = db.folder().getOutbox();
+ try {
+ db.folder().setFolderError(outbox.id, null);
+ db.folder().setFolderSyncState(outbox.id, "syncing");
+
+ ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
+
+ List ops = db.operation().getOperations(outbox.id);
+ Log.i(outbox.name + " pending operations=" + ops.size());
+ for (EntityOperation op : ops) {
+ EntityMessage message = db.message().getMessage(op.message);
+ try {
+ Log.i(outbox.name +
+ " start op=" + op.id + "/" + op.name +
+ " msg=" + op.message +
+ " args=" + op.args);
+
+ if (message == null)
+ throw new MessageRemovedException();
+
+ send(message);
+
+ db.operation().deleteOperation(op.id);
+ } catch (Throwable ex) {
+ Log.e(ex);
+
+ if (message != null)
+ db.message().setMessageError(message.id, Helper.formatThrowable(ex));
+
+ if (ex instanceof SendFailedException)
+ db.operation().deleteOperation(op.id);
+ else
+ throw ex;
+ } finally {
+ Log.i(outbox.name + " end op=" + op.id + "/" + op.name);
+ }
+
+ NetworkInfo ni = cm.getActiveNetworkInfo();
+ if (ni == null || !ni.isConnected())
+ break;
+ }
+
+ if (db.operation().getOperations(outbox.id).size() == 0)
+ stopSelf();
+ } catch (Throwable ex) {
+ Log.e(outbox.name, ex);
+ db.folder().setFolderError(outbox.id, Helper.formatThrowable(ex, true));
+ } finally {
+ db.folder().setFolderSyncState(outbox.id, null);
+ }
+ } finally {
+ wl.release();
+ }
+ }
+ });
+ thread.start();
+ }
+ }
+ };
+
+ private void send(EntityMessage message) throws MessagingException, IOException {
+ DB db = DB.getInstance(this);
+
+ // Mark attempt
+ if (message.last_attempt == null) {
+ message.last_attempt = new Date().getTime();
+ db.message().setMessageLastAttempt(message.id, message.last_attempt);
+ }
+
+ EntityIdentity ident = db.identity().getIdentity(message.identity);
+
+ // Get properties
+ Properties props = MessageHelper.getSessionProperties(ident.auth_type, ident.realm, ident.insecure);
+ InetAddress ip = (ident.use_ip ? Helper.getLocalIp(this) : null);
+ if (ip == null) {
+ EntityLog.log(this, "Send local host=" + ident.host);
+ if (ident.starttls)
+ props.put("mail.smtp.localhost", ident.host);
+ else
+ props.put("mail.smtps.localhost", ident.host);
+ } else {
+ InetAddress localhost = InetAddress.getLocalHost();
+ String haddr = "[" + (localhost instanceof Inet6Address ? "IPv6:" : "") + localhost.getHostAddress() + "]";
+ EntityLog.log(this, "Send local address=" + haddr);
+ if (ident.starttls)
+ props.put("mail.smtp.localhost", haddr);
+ else
+ props.put("mail.smtps.localhost", haddr);
+ }
+
+ // Create session
+ final Session isession = Session.getInstance(props, null);
+ isession.setDebug(true);
+
+ // Create message
+ MimeMessage imessage = MessageHelper.from(this, message, isession, ident.plain_only);
+
+ // Add reply to
+ if (ident.replyto != null)
+ imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
+
+ // Add bcc
+ if (ident.bcc != null) {
+ List bcc = new ArrayList<>();
+ Address[] existing = imessage.getRecipients(Message.RecipientType.BCC);
+ if (existing != null)
+ bcc.addAll(Arrays.asList(existing));
+ bcc.add(new InternetAddress(ident.bcc));
+ imessage.setRecipients(Message.RecipientType.BCC, bcc.toArray(new Address[0]));
+ }
+
+ // defacto standard
+ if (ident.delivery_receipt)
+ imessage.addHeader("Return-Receipt-To", ident.replyto == null ? ident.email : ident.replyto);
+
+ // https://tools.ietf.org/html/rfc3798
+ if (ident.read_receipt)
+ imessage.addHeader("Disposition-Notification-To", ident.replyto == null ? ident.email : ident.replyto);
+
+ // Create transport
+ // TODO: cache transport?
+ try (Transport itransport = isession.getTransport(ident.getProtocol())) {
+ // Connect transport
+ db.identity().setIdentityState(ident.id, "connecting");
+ try {
+ itransport.connect(ident.host, ident.port, ident.user, ident.password);
+ } catch (AuthenticationFailedException ex) {
+ if (ident.auth_type == Helper.AUTH_TYPE_GMAIL) {
+ EntityAccount account = db.account().getAccount(ident.account);
+ ident.password = Helper.refreshToken(this, "com.google", ident.user, account.password);
+ DB.getInstance(this).identity().setIdentityPassword(ident.id, ident.password);
+ itransport.connect(ident.host, ident.port, ident.user, ident.password);
+ } else
+ throw ex;
+ }
+
+ db.identity().setIdentityState(ident.id, "connected");
+
+ // Send message
+ Address[] to = imessage.getAllRecipients();
+ itransport.sendMessage(imessage, to);
+ EntityLog.log(this, "Sent via " + ident.host + "/" + ident.user +
+ " to " + TextUtils.join(", ", to));
+
+ // Append replied/forwarded text
+ StringBuilder sb = new StringBuilder();
+ sb.append(Helper.readText(EntityMessage.getFile(this, message.id)));
+ File refFile = EntityMessage.getRefFile(this, message.id);
+ if (refFile.exists())
+ sb.append(Helper.readText(refFile));
+ Helper.writeText(EntityMessage.getFile(this, message.id), sb.toString());
+
+ try {
+ db.beginTransaction();
+
+ db.message().setMessageSent(message.id, imessage.getSentDate().getTime());
+ db.message().setMessageSeen(message.id, true);
+ db.message().setMessageUiSeen(message.id, true);
+ db.message().setMessageError(message.id, null);
+
+ EntityFolder sent = db.folder().getFolderByType(message.account, EntityFolder.SENT);
+ if (ident.store_sent && sent != null) {
+ db.message().setMessageFolder(message.id, sent.id);
+ message.folder = sent.id;
+ EntityOperation.queue(this, db, message, EntityOperation.ADD);
+ } else
+ db.message().setMessageUiHide(message.id, true);
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ if (refFile.exists())
+ refFile.delete();
+
+ if (message.inreplyto != null) {
+ List replieds = db.message().getMessageByMsgId(message.account, message.inreplyto);
+ for (EntityMessage replied : replieds)
+ EntityOperation.queue(this, db, replied, EntityOperation.ANSWERED, true);
+ }
+
+ db.identity().setIdentityConnected(ident.id, new Date().getTime());
+ db.identity().setIdentityError(ident.id, null);
+
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.cancel("send", message.identity.intValue());
+
+ if (message.to != null)
+ for (Address recipient : message.to) {
+ String email = ((InternetAddress) recipient).getAddress();
+ String name = ((InternetAddress) recipient).getPersonal();
+ List contacts = db.contact().getContacts(EntityContact.TYPE_TO, email);
+ if (contacts.size() == 0) {
+ EntityContact contact = new EntityContact();
+ contact.type = EntityContact.TYPE_TO;
+ contact.email = email;
+ contact.name = name;
+ db.contact().insertContact(contact);
+ Log.i("Inserted recipient contact=" + contact);
+ } else {
+ EntityContact contact = contacts.get(0);
+ if (name != null && !name.equals(contact.name)) {
+ contact.name = name;
+ db.contact().updateContact(contact);
+ Log.i("Updated recipient contact=" + contact);
+ }
+ }
+ }
+ } catch (MessagingException ex) {
+ if (ex instanceof SendFailedException) {
+ SendFailedException sfe = (SendFailedException) ex;
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(sfe.getMessage());
+
+ sb.append(' ').append(getString(R.string.title_address_sent));
+ sb.append(' ').append(MessageHelper.formatAddresses(sfe.getValidSentAddresses()));
+
+ sb.append(' ').append(getString(R.string.title_address_unsent));
+ sb.append(' ').append(MessageHelper.formatAddresses(sfe.getValidUnsentAddresses()));
+
+ sb.append(' ').append(getString(R.string.title_address_invalid));
+ sb.append(' ').append(MessageHelper.formatAddresses(sfe.getInvalidAddresses()));
+
+ ex = new SendFailedException(
+ sb.toString(),
+ sfe.getNextException(),
+ sfe.getValidSentAddresses(),
+ sfe.getValidUnsentAddresses(),
+ sfe.getInvalidAddresses());
+ }
+
+ db.identity().setIdentityError(ident.id, Helper.formatThrowable(ex));
+
+ EntityLog.log(this, ident.name + " last attempt: " + new Date(message.last_attempt));
+
+ long now = new Date().getTime();
+ long delayed = now - message.last_attempt;
+ if (delayed > IDENTITY_ERROR_AFTER * 60 * 1000L) {
+ Log.i("Reporting send error after=" + delayed);
+ NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
+ nm.notify("send", message.identity.intValue(),
+ Helper.getNotificationError(this, ident.name, ex).build());
+ }
+
+ throw ex;
+ } finally {
+ db.identity().setIdentityState(ident.id, null);
+ }
+ }
+}
diff --git a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
index 077127104e..e2355e347b 100644
--- a/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
+++ b/app/src/main/java/eu/faircode/email/ServiceSynchronize.java
@@ -71,8 +71,6 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
-import java.net.Inet6Address;
-import java.net.InetAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
@@ -108,7 +106,6 @@ import javax.mail.SendFailedException;
import javax.mail.Session;
import javax.mail.Store;
import javax.mail.StoreClosedException;
-import javax.mail.Transport;
import javax.mail.UIDFolder;
import javax.mail.event.ConnectionAdapter;
import javax.mail.event.ConnectionEvent;
@@ -153,7 +150,6 @@ public class ServiceSynchronize extends LifecycleService {
private static final long RECONNECT_BACKOFF = 90 * 1000L; // milliseconds
private static final int ACCOUNT_ERROR_AFTER = 90; // minutes
private static final int BACKOFF_ERROR_AFTER = 16; // seconds
- private static final int IDENTITY_ERROR_AFTER = 30; // minutes
private static final long STOP_DELAY = 5000L; // milliseconds
private static final long YIELD_DURATION = 200L; // milliseconds
@@ -717,40 +713,6 @@ public class ServiceSynchronize extends LifecycleService {
return notifications;
}
- private NotificationCompat.Builder getNotificationError(String title, Throwable ex) {
- return getNotificationError("error", title, ex, true);
- }
-
- private NotificationCompat.Builder getNotificationError(String channel, String title, Throwable ex, boolean debug) {
- // Build pending intent
- Intent intent = new Intent(this, ActivitySetup.class);
- if (debug)
- intent.setAction("error");
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- PendingIntent pi = PendingIntent.getActivity(
- this, ActivitySetup.REQUEST_ERROR, intent, PendingIntent.FLAG_UPDATE_CURRENT);
-
- // Build notification
- NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channel);
-
- builder
- .setSmallIcon(R.drawable.baseline_warning_white_24)
- .setContentTitle(getString(R.string.title_notification_failed, title))
- .setContentText(Helper.formatThrowable(ex))
- .setContentIntent(pi)
- .setAutoCancel(false)
- .setShowWhen(true)
- .setPriority(Notification.PRIORITY_MAX)
- .setOnlyAlertOnce(true)
- .setCategory(Notification.CATEGORY_ERROR)
- .setVisibility(NotificationCompat.VISIBILITY_SECRET);
-
- builder.setStyle(new NotificationCompat.BigTextStyle()
- .bigText(Helper.formatThrowable(ex, false, "\n")));
-
- return builder;
- }
-
private void reportError(EntityAccount account, EntityFolder folder, Throwable ex) {
// FolderClosedException: can happen when no connectivity
@@ -778,7 +740,7 @@ public class ServiceSynchronize extends LifecycleService {
if ((ex instanceof SendFailedException) || (ex instanceof AlertException)) {
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- nm.notify(tag, 1, getNotificationError(title, ex).build());
+ nm.notify(tag, 1, Helper.getNotificationError(this, title, ex).build());
}
// connection failure: Too many simultaneous connections
@@ -798,7 +760,7 @@ public class ServiceSynchronize extends LifecycleService {
!(ex instanceof MessagingException && ex.getCause() instanceof SSLException) &&
!(ex instanceof MessagingException && "connection failure".equals(ex.getMessage()))) {
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- nm.notify(tag, 1, getNotificationError(title, ex).build());
+ nm.notify(tag, 1, Helper.getNotificationError(this, title, ex).build());
}
}
@@ -934,7 +896,8 @@ public class ServiceSynchronize extends LifecycleService {
.format((account.last_connected))), ex);
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify("receive", account.id.intValue(),
- getNotificationError("warning", account.name, warning, false).build());
+ Helper.getNotificationError(this, "warning", account.name, warning, false)
+ .build());
}
}
@@ -1511,10 +1474,6 @@ public class ServiceSynchronize extends LifecycleService {
doDelete(folder, (IMAPFolder) ifolder, message, jargs, db);
break;
- case EntityOperation.SEND:
- doSend(message, db);
- break;
-
case EntityOperation.HEADERS:
doHeaders(folder, (IMAPFolder) ifolder, message, db);
break;
@@ -1559,7 +1518,6 @@ public class ServiceSynchronize extends LifecycleService {
if (ex instanceof MessageRemovedException ||
ex instanceof FolderNotFoundException ||
- ex instanceof SendFailedException ||
ex instanceof IllegalArgumentException) {
Log.w("Unrecoverable", ex);
@@ -1905,197 +1863,6 @@ public class ServiceSynchronize extends LifecycleService {
db.message().deleteMessage(message.id);
}
- private void doSend(EntityMessage message, DB db) throws MessagingException, IOException {
- // Send message
- EntityIdentity ident = db.identity().getIdentity(message.identity);
-
- // Mark attempt
- if (message.last_attempt == null) {
- message.last_attempt = new Date().getTime();
- db.message().setMessageLastAttempt(message.id, message.last_attempt);
- }
-
- // Get properties
- Properties props = MessageHelper.getSessionProperties(ident.auth_type, ident.realm, ident.insecure);
- InetAddress ip = (ident.use_ip ? Helper.getLocalIp(ServiceSynchronize.this) : null);
- if (ip == null) {
- EntityLog.log(ServiceSynchronize.this, "Send local host=" + ident.host);
- if (ident.starttls)
- props.put("mail.smtp.localhost", ident.host);
- else
- props.put("mail.smtps.localhost", ident.host);
- } else {
- InetAddress localhost = InetAddress.getLocalHost();
- String haddr = "[" + (localhost instanceof Inet6Address ? "IPv6:" : "") + localhost.getHostAddress() + "]";
- EntityLog.log(ServiceSynchronize.this, "Send local address=" + haddr);
- if (ident.starttls)
- props.put("mail.smtp.localhost", haddr);
- else
- props.put("mail.smtps.localhost", haddr);
- }
-
- // Create session
- final Session isession = Session.getInstance(props, null);
- isession.setDebug(true);
-
- // Create message
- MimeMessage imessage = MessageHelper.from(this, message, isession, ident.plain_only);
-
- // Add reply to
- if (ident.replyto != null)
- imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
-
- // Add bcc
- if (ident.bcc != null) {
- List bcc = new ArrayList<>();
- Address[] existing = imessage.getRecipients(Message.RecipientType.BCC);
- if (existing != null)
- bcc.addAll(Arrays.asList(existing));
- bcc.add(new InternetAddress(ident.bcc));
- imessage.setRecipients(Message.RecipientType.BCC, bcc.toArray(new Address[0]));
- }
-
- // defacto standard
- if (ident.delivery_receipt)
- imessage.addHeader("Return-Receipt-To", ident.replyto == null ? ident.email : ident.replyto);
-
- // https://tools.ietf.org/html/rfc3798
- if (ident.read_receipt)
- imessage.addHeader("Disposition-Notification-To", ident.replyto == null ? ident.email : ident.replyto);
-
- // Create transport
- // TODO: cache transport?
- try (Transport itransport = isession.getTransport(ident.getProtocol())) {
- // Connect transport
- db.identity().setIdentityState(ident.id, "connecting");
- try {
- itransport.connect(ident.host, ident.port, ident.user, ident.password);
- } catch (AuthenticationFailedException ex) {
- if (ident.auth_type == Helper.AUTH_TYPE_GMAIL) {
- EntityAccount account = db.account().getAccount(ident.account);
- ident.password = Helper.refreshToken(this, "com.google", ident.user, account.password);
- DB.getInstance(this).identity().setIdentityPassword(ident.id, ident.password);
- itransport.connect(ident.host, ident.port, ident.user, ident.password);
- } else
- throw ex;
- }
-
- db.identity().setIdentityState(ident.id, "connected");
-
- // Send message
- Address[] to = imessage.getAllRecipients();
- itransport.sendMessage(imessage, to);
- EntityLog.log(this, "Sent via " + ident.host + "/" + ident.user +
- " to " + TextUtils.join(", ", to));
-
- // Append replied/forwarded text
- StringBuilder sb = new StringBuilder();
- sb.append(Helper.readText(EntityMessage.getFile(this, message.id)));
- File refFile = EntityMessage.getRefFile(this, message.id);
- if (refFile.exists())
- sb.append(Helper.readText(refFile));
- Helper.writeText(EntityMessage.getFile(this, message.id), sb.toString());
-
- try {
- db.beginTransaction();
-
- db.message().setMessageSent(message.id, imessage.getSentDate().getTime());
- db.message().setMessageSeen(message.id, true);
- db.message().setMessageUiSeen(message.id, true);
- db.message().setMessageError(message.id, null);
-
- EntityFolder sent = db.folder().getFolderByType(message.account, EntityFolder.SENT);
- if (ident.store_sent && sent != null) {
- db.message().setMessageFolder(message.id, sent.id);
- message.folder = sent.id;
- EntityOperation.queue(this, db, message, EntityOperation.ADD);
- } else
- db.message().setMessageUiHide(message.id, true);
-
- db.setTransactionSuccessful();
- } finally {
- db.endTransaction();
- }
-
- if (refFile.exists())
- refFile.delete();
-
- if (message.inreplyto != null) {
- List replieds = db.message().getMessageByMsgId(message.account, message.inreplyto);
- for (EntityMessage replied : replieds)
- EntityOperation.queue(this, db, replied, EntityOperation.ANSWERED, true);
- }
-
- db.identity().setIdentityConnected(ident.id, new Date().getTime());
- db.identity().setIdentityError(ident.id, null);
-
- NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- nm.cancel("send", message.identity.intValue());
-
- if (message.to != null)
- for (Address recipient : message.to) {
- String email = ((InternetAddress) recipient).getAddress();
- String name = ((InternetAddress) recipient).getPersonal();
- List contacts = db.contact().getContacts(EntityContact.TYPE_TO, email);
- if (contacts.size() == 0) {
- EntityContact contact = new EntityContact();
- contact.type = EntityContact.TYPE_TO;
- contact.email = email;
- contact.name = name;
- db.contact().insertContact(contact);
- Log.i("Inserted recipient contact=" + contact);
- } else {
- EntityContact contact = contacts.get(0);
- if (name != null && !name.equals(contact.name)) {
- contact.name = name;
- db.contact().updateContact(contact);
- Log.i("Updated recipient contact=" + contact);
- }
- }
- }
- } catch (MessagingException ex) {
- if (ex instanceof SendFailedException) {
- SendFailedException sfe = (SendFailedException) ex;
-
- StringBuilder sb = new StringBuilder();
-
- sb.append(sfe.getMessage());
-
- sb.append(' ').append(getString(R.string.title_address_sent));
- sb.append(' ').append(MessageHelper.formatAddresses(sfe.getValidSentAddresses()));
-
- sb.append(' ').append(getString(R.string.title_address_unsent));
- sb.append(' ').append(MessageHelper.formatAddresses(sfe.getValidUnsentAddresses()));
-
- sb.append(' ').append(getString(R.string.title_address_invalid));
- sb.append(' ').append(MessageHelper.formatAddresses(sfe.getInvalidAddresses()));
-
- ex = new SendFailedException(
- sb.toString(),
- sfe.getNextException(),
- sfe.getValidSentAddresses(),
- sfe.getValidUnsentAddresses(),
- sfe.getInvalidAddresses());
- }
-
- db.identity().setIdentityError(ident.id, Helper.formatThrowable(ex));
-
- EntityLog.log(this, ident.name + " last attempt: " + new Date(message.last_attempt));
-
- long now = new Date().getTime();
- long delayed = now - message.last_attempt;
- if (delayed > IDENTITY_ERROR_AFTER * 60 * 1000L) {
- Log.i("Reporting send error after=" + delayed);
- NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
- nm.notify("send", message.identity.intValue(), getNotificationError(ident.name, ex).build());
- }
-
- throw ex;
- } finally {
- db.identity().setIdentityState(ident.id, null);
- }
- }
-
private void doHeaders(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, DB db) throws MessagingException {
if (message.headers != null)
return;
@@ -3115,76 +2882,6 @@ public class ServiceSynchronize extends LifecycleService {
Log.w("main backoff " + ex.toString());
}
- // Start monitoring outbox
- Handler handler = null;
- final EntityFolder outbox = db.folder().getOutbox();
- if (outbox != null) {
- db.folder().setFolderError(outbox.id, null);
-
- handler = new Handler(getMainLooper()) {
- private List handling = new ArrayList<>();
- private LiveData> liveOperations;
-
- @Override
- public void handleMessage(android.os.Message msg) {
- Log.i(outbox.name + " observe=" + msg.what);
- if (msg.what == 0) {
- liveOperations.removeObserver(observer);
- handling.clear();
- } else {
- liveOperations = db.operation().liveOperations(outbox.id);
- liveOperations.observe(ServiceSynchronize.this, observer);
- }
- }
-
- private Observer> observer = new Observer>() {
- private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
- PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
- PowerManager.WakeLock wl = pm.newWakeLock(
- PowerManager.PARTIAL_WAKE_LOCK, BuildConfig.APPLICATION_ID + ":outbox");
-
- @Override
- public void onChanged(final List operations) {
- boolean process = false;
- List current = new ArrayList<>();
- for (EntityOperation op : operations) {
- if (!handling.contains(op.id))
- process = true;
- current.add(op.id);
- }
- handling = current;
-
- if (handling.size() > 0 && process) {
- Log.i(outbox.name + " operations=" + operations.size());
- executor.submit(new Runnable() {
- @Override
- public void run() {
- try {
- wl.acquire();
- Log.i(outbox.name + " process");
-
- db.folder().setFolderSyncState(outbox.id, "syncing");
- processOperations(null, outbox, null, null, null, state);
- db.folder().setFolderError(outbox.id, null);
- } catch (Throwable ex) {
- Log.e(outbox.name, ex);
- reportError(null, outbox, ex);
- db.folder().setFolderError(outbox.id, Helper.formatThrowable(ex, true));
- } finally {
- db.folder().setFolderSyncState(outbox.id, null);
- wl.release();
- EntityLog.log(ServiceSynchronize.this, "Outbox wake lock=" + wl.isHeld());
- }
- }
- });
- }
- }
- };
- };
- handler.sendEmptyMessage(1);
- db.folder().setFolderState(outbox.id, "connected");
- }
-
// Start monitoring accounts
List accounts = db.account().getSynchronizingAccounts(false);
for (final EntityAccount account : accounts) {
@@ -3230,13 +2927,6 @@ public class ServiceSynchronize extends LifecycleService {
astate.join();
threadState.clear();
- // Stop monitoring outbox
- if (outbox != null) {
- Log.i(outbox.name + " unlisten operations");
- handler.sendEmptyMessage(0);
- db.folder().setFolderState(outbox.id, null);
- }
-
EntityLog.log(ServiceSynchronize.this, "Main exited");
} catch (Throwable ex) {
// Fail-safe
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 40935c26d7..985c7d7e17 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -59,6 +59,7 @@
Report message from %1$s as spam?
+ Sending messages
\'%1$s\' failed
Templates