Use dedicated server for sending messages

pull/152/head
M66B 7 years ago
parent 6451036b25
commit f36b430c1b

@ -162,6 +162,8 @@
<service android:name=".ServiceSynchronize" /> <service android:name=".ServiceSynchronize" />
<service android:name=".ServiceSend" />
<service android:name=".ServiceExternal"> <service android:name=".ServiceExternal">
<intent-filter> <intent-filter>
<action android:name="${applicationId}.ENABLE" /> <action android:name="${applicationId}.ENABLE" />

@ -20,6 +20,7 @@ package eu.faircode.email;
*/ */
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences; import android.content.SharedPreferences;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
@ -221,6 +222,9 @@ public class EntityOperation {
} else if (DELETE.equals(name)) } else if (DELETE.equals(name))
db.message().setMessageUiHide(message.id, true); db.message().setMessageUiHide(message.id, true);
else if (SEND.equals(name))
context.startService(new Intent(context, ServiceSend.class));
} catch (JSONException ex) { } catch (JSONException ex) {
Log.e(ex); Log.e(ex);
} }

@ -21,6 +21,8 @@ package eu.faircode.email;
import android.accounts.Account; import android.accounts.Account;
import android.accounts.AccountManager; import android.accounts.AccountManager;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManager;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Context; import android.content.Context;
@ -100,6 +102,7 @@ import javax.net.ssl.SSLException;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.browser.customtabs.CustomTabsIntent; import androidx.browser.customtabs.CustomTabsIntent;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.lifecycle.Lifecycle; import androidx.lifecycle.Lifecycle;
import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.LifecycleOwner;
@ -109,7 +112,8 @@ import static androidx.browser.customtabs.CustomTabsService.ACTION_CUSTOM_TABS_C
public class Helper { public class Helper {
static final int NOTIFICATION_SYNCHRONIZE = 1; 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; static final int JOB_DAILY = 1001;
@ -1035,4 +1039,38 @@ public class Helper {
static String sanitizeFilename(String name) { static String sanitizeFilename(String name) {
return (name == null ? null : name.replaceAll("[^a-zA-Z0-9\\.\\-]", "_")); 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;
}
} }

@ -33,6 +33,7 @@ public class ReceiverAutostart extends BroadcastReceiver {
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) || if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction()) ||
Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) { Intent.ACTION_MY_PACKAGE_REPLACED.equals(intent.getAction())) {
EntityLog.log(context, intent.getAction()); EntityLog.log(context, intent.getAction());
ServiceSynchronize.init(context, true); ServiceSynchronize.init(context, true);
Thread thread = new Thread(new Runnable() { Thread thread = new Thread(new Runnable() {
@ -40,9 +41,18 @@ public class ReceiverAutostart extends BroadcastReceiver {
public void run() { public void run() {
try { try {
DB db = DB.getInstance(context); DB db = DB.getInstance(context);
List<EntityMessage> messages = db.message().getSnoozed(); List<EntityMessage> messages = db.message().getSnoozed();
for (EntityMessage message : messages) for (EntityMessage message : messages)
EntityMessage.snooze(context, message.id, message.ui_snoozed); 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) { } catch (Throwable ex) {
Log.e(ex); Log.e(ex);
} }

@ -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 <http://www.gnu.org/licenses/>.
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<EntityOperation> 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<Address> 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<EntityMessage> 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<EntityContact> 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);
}
}
}

@ -71,8 +71,6 @@ import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.SocketException; import java.net.SocketException;
import java.net.SocketTimeoutException; import java.net.SocketTimeoutException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
@ -108,7 +106,6 @@ import javax.mail.SendFailedException;
import javax.mail.Session; import javax.mail.Session;
import javax.mail.Store; import javax.mail.Store;
import javax.mail.StoreClosedException; import javax.mail.StoreClosedException;
import javax.mail.Transport;
import javax.mail.UIDFolder; import javax.mail.UIDFolder;
import javax.mail.event.ConnectionAdapter; import javax.mail.event.ConnectionAdapter;
import javax.mail.event.ConnectionEvent; 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 long RECONNECT_BACKOFF = 90 * 1000L; // milliseconds
private static final int ACCOUNT_ERROR_AFTER = 90; // minutes private static final int ACCOUNT_ERROR_AFTER = 90; // minutes
private static final int BACKOFF_ERROR_AFTER = 16; // seconds 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 STOP_DELAY = 5000L; // milliseconds
private static final long YIELD_DURATION = 200L; // milliseconds private static final long YIELD_DURATION = 200L; // milliseconds
@ -717,40 +713,6 @@ public class ServiceSynchronize extends LifecycleService {
return notifications; 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) { private void reportError(EntityAccount account, EntityFolder folder, Throwable ex) {
// FolderClosedException: can happen when no connectivity // FolderClosedException: can happen when no connectivity
@ -778,7 +740,7 @@ public class ServiceSynchronize extends LifecycleService {
if ((ex instanceof SendFailedException) || (ex instanceof AlertException)) { if ((ex instanceof SendFailedException) || (ex instanceof AlertException)) {
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 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 // 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 && ex.getCause() instanceof SSLException) &&
!(ex instanceof MessagingException && "connection failure".equals(ex.getMessage()))) { !(ex instanceof MessagingException && "connection failure".equals(ex.getMessage()))) {
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 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); .format((account.last_connected))), ex);
NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
nm.notify("receive", account.id.intValue(), 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); doDelete(folder, (IMAPFolder) ifolder, message, jargs, db);
break; break;
case EntityOperation.SEND:
doSend(message, db);
break;
case EntityOperation.HEADERS: case EntityOperation.HEADERS:
doHeaders(folder, (IMAPFolder) ifolder, message, db); doHeaders(folder, (IMAPFolder) ifolder, message, db);
break; break;
@ -1559,7 +1518,6 @@ public class ServiceSynchronize extends LifecycleService {
if (ex instanceof MessageRemovedException || if (ex instanceof MessageRemovedException ||
ex instanceof FolderNotFoundException || ex instanceof FolderNotFoundException ||
ex instanceof SendFailedException ||
ex instanceof IllegalArgumentException) { ex instanceof IllegalArgumentException) {
Log.w("Unrecoverable", ex); Log.w("Unrecoverable", ex);
@ -1905,197 +1863,6 @@ public class ServiceSynchronize extends LifecycleService {
db.message().deleteMessage(message.id); 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<Address> 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<EntityMessage> 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<EntityContact> 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 { private void doHeaders(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, DB db) throws MessagingException {
if (message.headers != null) if (message.headers != null)
return; return;
@ -3115,76 +2882,6 @@ public class ServiceSynchronize extends LifecycleService {
Log.w("main backoff " + ex.toString()); 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<Long> handling = new ArrayList<>();
private LiveData<List<EntityOperation>> 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<List<EntityOperation>> observer = new Observer<List<EntityOperation>>() {
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<EntityOperation> operations) {
boolean process = false;
List<Long> 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 // Start monitoring accounts
List<EntityAccount> accounts = db.account().getSynchronizingAccounts(false); List<EntityAccount> accounts = db.account().getSynchronizingAccounts(false);
for (final EntityAccount account : accounts) { for (final EntityAccount account : accounts) {
@ -3230,13 +2927,6 @@ public class ServiceSynchronize extends LifecycleService {
astate.join(); astate.join();
threadState.clear(); 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"); EntityLog.log(ServiceSynchronize.this, "Main exited");
} catch (Throwable ex) { } catch (Throwable ex) {
// Fail-safe // Fail-safe

@ -59,6 +59,7 @@
</plurals> </plurals>
<string name="title_ask_spam_who">Report message from %1$s as spam?</string> <string name="title_ask_spam_who">Report message from %1$s as spam?</string>
<string name="title_notification_sending">Sending messages</string>
<string name="title_notification_failed">\'%1$s\' failed</string> <string name="title_notification_failed">\'%1$s\' failed</string>
<string name="menu_answers">Templates</string> <string name="menu_answers">Templates</string>

Loading…
Cancel
Save