Simple email application for Android. Original source code: https://framagit.org/dystopia-project/simple-email

2070 lines
95 KiB

6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
6 years ago
  1. package eu.faircode.email;
  2. /*
  3. This file is part of FairEmail.
  4. FairEmail is free software: you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation, either version 3 of the License, or
  7. (at your option) any later version.
  8. NetGuard is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License
  13. along with NetGuard. If not, see <http://www.gnu.org/licenses/>.
  14. Copyright 2018 by Marcel Bokhorst (M66B)
  15. */
  16. import android.Manifest;
  17. import android.app.Notification;
  18. import android.app.NotificationManager;
  19. import android.app.PendingIntent;
  20. import android.content.BroadcastReceiver;
  21. import android.content.ContentResolver;
  22. import android.content.ContentUris;
  23. import android.content.Context;
  24. import android.content.Intent;
  25. import android.content.IntentFilter;
  26. import android.content.SharedPreferences;
  27. import android.content.pm.PackageManager;
  28. import android.database.Cursor;
  29. import android.graphics.drawable.Icon;
  30. import android.media.RingtoneManager;
  31. import android.net.ConnectivityManager;
  32. import android.net.Network;
  33. import android.net.NetworkCapabilities;
  34. import android.net.NetworkInfo;
  35. import android.net.NetworkRequest;
  36. import android.net.Uri;
  37. import android.os.Build;
  38. import android.os.Bundle;
  39. import android.os.SystemClock;
  40. import android.preference.PreferenceManager;
  41. import android.provider.ContactsContract;
  42. import android.text.Html;
  43. import android.text.TextUtils;
  44. import android.util.Log;
  45. import com.sun.mail.iap.ConnectionException;
  46. import com.sun.mail.imap.AppendUID;
  47. import com.sun.mail.imap.IMAPFolder;
  48. import com.sun.mail.imap.IMAPMessage;
  49. import com.sun.mail.imap.IMAPStore;
  50. import com.sun.mail.util.FolderClosedIOException;
  51. import com.sun.mail.util.MailConnectException;
  52. import org.json.JSONArray;
  53. import org.json.JSONException;
  54. import java.io.IOException;
  55. import java.net.SocketException;
  56. import java.net.SocketTimeoutException;
  57. import java.net.UnknownHostException;
  58. import java.text.DateFormat;
  59. import java.text.SimpleDateFormat;
  60. import java.util.ArrayList;
  61. import java.util.Arrays;
  62. import java.util.Calendar;
  63. import java.util.Date;
  64. import java.util.Enumeration;
  65. import java.util.HashMap;
  66. import java.util.List;
  67. import java.util.Map;
  68. import java.util.Properties;
  69. import java.util.concurrent.ExecutorService;
  70. import java.util.concurrent.Executors;
  71. import javax.mail.Address;
  72. import javax.mail.AuthenticationFailedException;
  73. import javax.mail.FetchProfile;
  74. import javax.mail.Flags;
  75. import javax.mail.Folder;
  76. import javax.mail.FolderClosedException;
  77. import javax.mail.FolderNotFoundException;
  78. import javax.mail.Header;
  79. import javax.mail.Message;
  80. import javax.mail.MessageRemovedException;
  81. import javax.mail.MessagingException;
  82. import javax.mail.NoSuchProviderException;
  83. import javax.mail.SendFailedException;
  84. import javax.mail.Session;
  85. import javax.mail.StoreClosedException;
  86. import javax.mail.Transport;
  87. import javax.mail.UIDFolder;
  88. import javax.mail.event.ConnectionAdapter;
  89. import javax.mail.event.ConnectionEvent;
  90. import javax.mail.event.FolderAdapter;
  91. import javax.mail.event.FolderEvent;
  92. import javax.mail.event.MessageChangedEvent;
  93. import javax.mail.event.MessageChangedListener;
  94. import javax.mail.event.MessageCountAdapter;
  95. import javax.mail.event.MessageCountEvent;
  96. import javax.mail.event.StoreEvent;
  97. import javax.mail.event.StoreListener;
  98. import javax.mail.internet.InternetAddress;
  99. import javax.mail.internet.MimeMessage;
  100. import javax.mail.search.ComparisonTerm;
  101. import javax.mail.search.ReceivedDateTerm;
  102. import javax.net.ssl.SSLException;
  103. import androidx.annotation.Nullable;
  104. import androidx.core.content.ContextCompat;
  105. import androidx.lifecycle.LifecycleService;
  106. import androidx.lifecycle.Observer;
  107. import androidx.localbroadcastmanager.content.LocalBroadcastManager;
  108. import static android.os.Process.THREAD_PRIORITY_BACKGROUND;
  109. public class ServiceSynchronize extends LifecycleService {
  110. private final Object lock = new Object();
  111. private ServiceManager serviceManager = new ServiceManager();
  112. private static final int NOTIFICATION_SYNCHRONIZE = 1;
  113. private static final int CONNECT_BACKOFF_START = 8; // seconds
  114. private static final int CONNECT_BACKOFF_MAX = 1024; // seconds (1024 sec ~ 17 min)
  115. private static final int SYNC_BATCH_SIZE = 20;
  116. private static final int DOWNLOAD_BATCH_SIZE = 20;
  117. private static final int MESSAGE_AUTO_DOWNLOAD_SIZE = 32 * 1024; // bytes
  118. private static final int ATTACHMENT_AUTO_DOWNLOAD_SIZE = 32 * 1024; // bytes
  119. private static final long RECONNECT_BACKOFF = 90 * 1000L; // milliseconds
  120. static final int PI_UNSEEN = 1;
  121. static final int PI_SEEN = 2;
  122. static final int PI_TRASH = 3;
  123. static final String ACTION_SYNCHRONIZE_FOLDER = BuildConfig.APPLICATION_ID + ".SYNCHRONIZE_FOLDER";
  124. static final String ACTION_PROCESS_OPERATIONS = BuildConfig.APPLICATION_ID + ".PROCESS_OPERATIONS";
  125. @Override
  126. public void onCreate() {
  127. Log.i(Helper.TAG, "Service create version=" + BuildConfig.VERSION_NAME);
  128. super.onCreate();
  129. startForeground(NOTIFICATION_SYNCHRONIZE, getNotificationService(0, 0, 0).build());
  130. // Listen for network changes
  131. ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
  132. NetworkRequest.Builder builder = new NetworkRequest.Builder();
  133. builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
  134. // Removed because of Android VPN service
  135. // builder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
  136. cm.registerNetworkCallback(builder.build(), serviceManager);
  137. DB db = DB.getInstance(this);
  138. db.account().liveStats().observe(this, new Observer<TupleAccountStats>() {
  139. @Override
  140. public void onChanged(@Nullable TupleAccountStats stats) {
  141. NotificationManager nm = getSystemService(NotificationManager.class);
  142. nm.notify(NOTIFICATION_SYNCHRONIZE,
  143. getNotificationService(stats.accounts, stats.operations, stats.unsent).build());
  144. }
  145. });
  146. db.message().liveUnseenUnified().observe(this, new Observer<List<EntityMessage>>() {
  147. private List<Integer> notifying = new ArrayList<>();
  148. @Override
  149. public void onChanged(List<EntityMessage> messages) {
  150. NotificationManager nm = getSystemService(NotificationManager.class);
  151. List<Notification> notifications = getNotificationUnseen(messages);
  152. List<Integer> all = new ArrayList<>();
  153. List<Integer> added = new ArrayList<>();
  154. List<Integer> removed = new ArrayList<>(notifying);
  155. for (Notification notification : notifications) {
  156. Integer id = (int) notification.extras.getLong("id", 0);
  157. if (id > 0) {
  158. all.add(id);
  159. if (removed.contains(id))
  160. removed.remove(id);
  161. else
  162. added.add(id);
  163. }
  164. }
  165. if (notifications.size() == 0)
  166. nm.cancel("unseen", 0);
  167. for (Integer id : removed)
  168. nm.cancel("unseen", id);
  169. for (Notification notification : notifications) {
  170. Integer id = (int) notification.extras.getLong("id", 0);
  171. if ((id == 0 && added.size() + removed.size() > 0) || added.contains(id))
  172. nm.notify("unseen", id, notification);
  173. }
  174. notifying = all;
  175. }
  176. });
  177. }
  178. @Override
  179. public void onDestroy() {
  180. Log.i(Helper.TAG, "Service destroy");
  181. ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
  182. cm.unregisterNetworkCallback(serviceManager);
  183. serviceManager.onLost(null);
  184. stopForeground(true);
  185. NotificationManager nm = getSystemService(NotificationManager.class);
  186. nm.cancel(NOTIFICATION_SYNCHRONIZE);
  187. super.onDestroy();
  188. }
  189. @Override
  190. public int onStartCommand(Intent intent, int flags, int startId) {
  191. Log.i(Helper.TAG, "Service command intent=" + intent);
  192. super.onStartCommand(intent, flags, startId);
  193. if (intent != null) {
  194. String action = intent.getAction();
  195. if ("reload".equals(action))
  196. serviceManager.restart();
  197. else if ("until".equals(action)) {
  198. Bundle args = new Bundle();
  199. args.putLong("time", new Date().getTime());
  200. new SimpleTask<Void>() {
  201. @Override
  202. protected Void onLoad(Context context, Bundle args) {
  203. long time = args.getLong("time");
  204. DB db = DB.getInstance(context);
  205. try {
  206. db.beginTransaction();
  207. for (EntityAccount account : db.account().getAccounts(true))
  208. db.account().setAccountSeenUntil(account.id, time);
  209. db.setTransactionSuccessful();
  210. } finally {
  211. db.endTransaction();
  212. }
  213. return null;
  214. }
  215. @Override
  216. protected void onLoaded(Bundle args, Void data) {
  217. Log.i(Helper.TAG, "Updated seen until");
  218. }
  219. }.load(this, args);
  220. } else if (action != null &&
  221. (action.startsWith("seen:") || action.startsWith("trash:"))) {
  222. Bundle args = new Bundle();
  223. args.putLong("id", Long.parseLong(action.split(":")[1]));
  224. args.putString("action", action.split(":")[0]);
  225. new SimpleTask<Void>() {
  226. @Override
  227. protected Void onLoad(Context context, Bundle args) {
  228. long id = args.getLong("id");
  229. String action = args.getString("action");
  230. DB db = DB.getInstance(context);
  231. try {
  232. db.beginTransaction();
  233. EntityMessage message = db.message().getMessage(id);
  234. if ("seen".equals(action)) {
  235. db.message().setMessageUiSeen(message.id, true);
  236. EntityOperation.queue(db, message, EntityOperation.SEEN, true);
  237. } else if ("trash".equals(action)) {
  238. db.message().setMessageUiHide(message.id, true);
  239. EntityFolder trash = db.folder().getFolderByType(message.account, EntityFolder.TRASH);
  240. if (trash != null)
  241. EntityOperation.queue(db, message, EntityOperation.MOVE, trash.id);
  242. }
  243. db.setTransactionSuccessful();
  244. } finally {
  245. db.endTransaction();
  246. }
  247. EntityOperation.process(context);
  248. return null;
  249. }
  250. @Override
  251. protected void onLoaded(Bundle args, Void data) {
  252. Log.i(Helper.TAG, "Set seen");
  253. }
  254. }.load(this, args);
  255. }
  256. }
  257. return START_STICKY;
  258. }
  259. private Notification.Builder getNotificationService(int accounts, int operations, int unsent) {
  260. // Build pending intent
  261. Intent intent = new Intent(this, ActivityView.class);
  262. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  263. PendingIntent pi = PendingIntent.getActivity(
  264. this, ActivityView.REQUEST_SERVICE, intent, PendingIntent.FLAG_UPDATE_CURRENT);
  265. // Build notification
  266. Notification.Builder builder;
  267. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
  268. builder = new Notification.Builder(this, "service");
  269. else
  270. builder = new Notification.Builder(this);
  271. builder
  272. .setSmallIcon(R.drawable.baseline_compare_arrows_white_24)
  273. .setContentTitle(getResources().getQuantityString(R.plurals.title_notification_synchronizing, accounts, accounts))
  274. .setContentIntent(pi)
  275. .setAutoCancel(false)
  276. .setShowWhen(false)
  277. .setPriority(Notification.PRIORITY_MIN)
  278. .setCategory(Notification.CATEGORY_STATUS)
  279. .setVisibility(Notification.VISIBILITY_SECRET);
  280. if (operations > 0)
  281. builder.setStyle(new Notification.BigTextStyle().setSummaryText(
  282. getResources().getQuantityString(R.plurals.title_notification_operations, operations, operations)));
  283. if (unsent > 0)
  284. builder.setContentText(getResources().getQuantityString(R.plurals.title_notification_unsent, unsent, unsent));
  285. return builder;
  286. }
  287. private List<Notification> getNotificationUnseen(List<EntityMessage> messages) {
  288. // https://developer.android.com/training/notify-user/group
  289. List<Notification> notifications = new ArrayList<>();
  290. if (messages.size() == 0)
  291. return notifications;
  292. boolean pro = Helper.isPro(this);
  293. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
  294. // Build pending intent
  295. Intent view = new Intent(this, ActivityView.class);
  296. view.setAction("notification");
  297. view.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  298. PendingIntent piView = PendingIntent.getActivity(
  299. this, ActivityView.REQUEST_UNSEEN, view, PendingIntent.FLAG_UPDATE_CURRENT);
  300. Intent until = new Intent(this, ServiceSynchronize.class);
  301. until.setAction("until");
  302. PendingIntent piUntil = PendingIntent.getService(
  303. this, PI_UNSEEN, until, PendingIntent.FLAG_UPDATE_CURRENT);
  304. // Build notification
  305. Notification.Builder builder;
  306. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
  307. builder = new Notification.Builder(this);
  308. else
  309. builder = new Notification.Builder(this, "notification");
  310. builder
  311. .setSmallIcon(R.drawable.baseline_email_white_24)
  312. .setContentTitle(getResources().getQuantityString(R.plurals.title_notification_unseen, messages.size(), messages.size()))
  313. .setContentText("")
  314. .setContentIntent(piView)
  315. .setNumber(messages.size())
  316. .setShowWhen(false)
  317. .setPriority(Notification.PRIORITY_DEFAULT)
  318. .setCategory(Notification.CATEGORY_STATUS)
  319. .setVisibility(Notification.VISIBILITY_PRIVATE)
  320. .setDeleteIntent(piUntil)
  321. .setGroup(BuildConfig.APPLICATION_ID)
  322. .setGroupSummary(true);
  323. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
  324. builder.setSound(null);
  325. else
  326. builder.setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN);
  327. if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.O &&
  328. prefs.getBoolean("light", false)) {
  329. builder.setDefaults(Notification.DEFAULT_SOUND | Notification.DEFAULT_LIGHTS);
  330. builder.setLights(0xff00ff00, 1000, 1000);
  331. }
  332. if (pro) {
  333. DateFormat df = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.SHORT, SimpleDateFormat.SHORT);
  334. StringBuilder sb = new StringBuilder();
  335. for (EntityMessage message : messages) {
  336. sb.append("<strong>").append(MessageHelper.getFormattedAddresses(message.from, false)).append("</strong>");
  337. if (!TextUtils.isEmpty(message.subject))
  338. sb.append(": ").append(message.subject);
  339. sb.append(" ").append(df.format(new Date(message.sent == null ? message.received : message.sent)));
  340. sb.append("<br>");
  341. }
  342. builder.setStyle(new Notification.BigTextStyle().bigText(Html.fromHtml(sb.toString())));
  343. }
  344. notifications.add(builder.build());
  345. Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
  346. for (EntityMessage message : messages) {
  347. Bundle args = new Bundle();
  348. args.putLong("id", message.id);
  349. Intent seen = new Intent(this, ServiceSynchronize.class);
  350. seen.setAction("seen:" + message.id);
  351. PendingIntent piSeen = PendingIntent.getService(this, PI_SEEN, seen, PendingIntent.FLAG_UPDATE_CURRENT);
  352. Intent trash = new Intent(this, ServiceSynchronize.class);
  353. trash.setAction("trash:" + message.id);
  354. PendingIntent piTrash = PendingIntent.getService(this, PI_TRASH, trash, PendingIntent.FLAG_UPDATE_CURRENT);
  355. Notification.Action.Builder actionSeen = new Notification.Action.Builder(
  356. Icon.createWithResource(this, R.drawable.baseline_visibility_24),
  357. getString(R.string.title_seen),
  358. piSeen);
  359. Notification.Action.Builder actionTrash = new Notification.Action.Builder(
  360. Icon.createWithResource(this, R.drawable.baseline_delete_24),
  361. getString(R.string.title_trash),
  362. piTrash);
  363. Notification.Builder mbuilder;
  364. if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
  365. mbuilder = new Notification.Builder(this);
  366. else
  367. mbuilder = new Notification.Builder(this, "notification");
  368. mbuilder
  369. .addExtras(args)
  370. .setSmallIcon(R.drawable.baseline_mail_24)
  371. .setContentTitle(MessageHelper.getFormattedAddresses(message.from, true))
  372. .setContentIntent(piView)
  373. .setSound(uri)
  374. .setWhen(message.sent == null ? message.received : message.sent)
  375. .setPriority(Notification.PRIORITY_DEFAULT)
  376. .setCategory(Notification.CATEGORY_STATUS)
  377. .setVisibility(Notification.VISIBILITY_PRIVATE)
  378. .setGroup(BuildConfig.APPLICATION_ID)
  379. .setGroupSummary(false)
  380. .addAction(actionSeen.build())
  381. .addAction(actionTrash.build());
  382. if (pro)
  383. if (!TextUtils.isEmpty(message.subject))
  384. mbuilder.setContentText(message.subject);
  385. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
  386. mbuilder.setGroupAlertBehavior(Notification.GROUP_ALERT_CHILDREN);
  387. notifications.add(mbuilder.build());
  388. }
  389. return notifications;
  390. }
  391. private Notification.Builder getNotificationError(String action, Throwable ex) {
  392. // Build pending intent
  393. Intent intent = new Intent(this, ActivityView.class);
  394. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  395. PendingIntent pi = PendingIntent.getActivity(
  396. this, ActivityView.REQUEST_ERROR, intent, PendingIntent.FLAG_UPDATE_CURRENT);
  397. // Build notification
  398. Notification.Builder builder;
  399. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
  400. builder = new Notification.Builder(this, "error");
  401. else
  402. builder = new Notification.Builder(this);
  403. builder
  404. .setSmallIcon(android.R.drawable.stat_notify_error)
  405. .setContentTitle(getString(R.string.title_notification_failed, action))
  406. .setContentText(Helper.formatThrowable(ex))
  407. .setContentIntent(pi)
  408. .setAutoCancel(false)
  409. .setShowWhen(true)
  410. .setPriority(Notification.PRIORITY_MAX)
  411. .setCategory(Notification.CATEGORY_ERROR)
  412. .setVisibility(Notification.VISIBILITY_SECRET);
  413. builder.setStyle(new Notification.BigTextStyle().bigText(ex.toString()));
  414. return builder;
  415. }
  416. private void reportError(String account, String folder, Throwable ex) {
  417. // FolderClosedException: can happen when no connectivity
  418. // IllegalStateException:
  419. // - "This operation is not allowed on a closed folder"
  420. // - can happen when syncing message
  421. // ConnectionException
  422. // - failed to create new store connection (connectivity)
  423. // MailConnectException
  424. // - on connectity problems when connecting to store
  425. String action;
  426. if (TextUtils.isEmpty(account))
  427. action = folder;
  428. else if (TextUtils.isEmpty(folder))
  429. action = account;
  430. else
  431. action = account + "/" + folder;
  432. EntityLog.log(this, action + " " + Helper.formatThrowable(ex));
  433. if (ex instanceof SendFailedException) {
  434. NotificationManager nm = getSystemService(NotificationManager.class);
  435. nm.notify(action, 1, getNotificationError(action, ex).build());
  436. }
  437. if (BuildConfig.DEBUG &&
  438. !(ex instanceof SendFailedException) &&
  439. !(ex instanceof MailConnectException) &&
  440. !(ex instanceof FolderClosedException) &&
  441. !(ex instanceof IllegalStateException) &&
  442. !(ex instanceof AuthenticationFailedException) && // Also: Too many simultaneous connections
  443. !(ex instanceof StoreClosedException) &&
  444. !(ex instanceof MessagingException && ex.getCause() instanceof UnknownHostException) &&
  445. !(ex instanceof MessagingException && ex.getCause() instanceof ConnectionException) &&
  446. !(ex instanceof MessagingException && ex.getCause() instanceof SocketException) &&
  447. !(ex instanceof MessagingException && ex.getCause() instanceof SocketTimeoutException) &&
  448. !(ex instanceof MessagingException && ex.getCause() instanceof SSLException) &&
  449. !(ex instanceof MessagingException && "connection failure".equals(ex.getMessage()))) {
  450. NotificationManager nm = getSystemService(NotificationManager.class);
  451. nm.notify(action, 1, getNotificationError(action, ex).build());
  452. }
  453. }
  454. private void monitorAccount(final EntityAccount account, final ServiceState state) throws NoSuchProviderException {
  455. final DB db = DB.getInstance(this);
  456. final ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
  457. int backoff = CONNECT_BACKOFF_START;
  458. while (state.running) {
  459. EntityLog.log(this, account.name + " run");
  460. // Debug
  461. boolean debug = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("debug", false);
  462. debug = debug || BuildConfig.DEBUG;
  463. System.setProperty("mail.socket.debug", Boolean.toString(debug));
  464. // Create session
  465. Properties props = MessageHelper.getSessionProperties(account.auth_type);
  466. final Session isession = Session.getInstance(props, null);
  467. isession.setDebug(debug);
  468. // adb -t 1 logcat | grep "fairemail\|System.out"
  469. final IMAPStore istore = (IMAPStore) isession.getStore("imaps");
  470. final Map<EntityFolder, IMAPFolder> folders = new HashMap<>();
  471. List<Thread> pollers = new ArrayList<>();
  472. List<Thread> idlers = new ArrayList<>();
  473. try {
  474. // Listen for store events
  475. istore.addStoreListener(new StoreListener() {
  476. @Override
  477. public void notification(StoreEvent e) {
  478. Log.i(Helper.TAG, account.name + " event: " + e.getMessage());
  479. db.account().setAccountError(account.id, e.getMessage());
  480. synchronized (state) {
  481. state.notifyAll();
  482. }
  483. }
  484. });
  485. // Listen for folder events
  486. istore.addFolderListener(new FolderAdapter() {
  487. @Override
  488. public void folderCreated(FolderEvent e) {
  489. Log.i(Helper.TAG, "Folder created=" + e.getFolder().getFullName());
  490. synchronized (state) {
  491. state.notifyAll();
  492. }
  493. }
  494. @Override
  495. public void folderRenamed(FolderEvent e) {
  496. Log.i(Helper.TAG, "Folder renamed=" + e.getFolder());
  497. String old = e.getFolder().getFullName();
  498. String name = e.getNewFolder().getFullName();
  499. int count = db.folder().renameFolder(account.id, old, name);
  500. Log.i(Helper.TAG, "Renamed to " + name + " count=" + count);
  501. synchronized (state) {
  502. state.notifyAll();
  503. }
  504. }
  505. @Override
  506. public void folderDeleted(FolderEvent e) {
  507. Log.i(Helper.TAG, "Folder deleted=" + e.getFolder().getFullName());
  508. synchronized (state) {
  509. state.notifyAll();
  510. }
  511. }
  512. });
  513. // Listen for connection events
  514. istore.addConnectionListener(new ConnectionAdapter() {
  515. @Override
  516. public void opened(ConnectionEvent e) {
  517. Log.i(Helper.TAG, account.name + " opened");
  518. }
  519. @Override
  520. public void disconnected(ConnectionEvent e) {
  521. Log.e(Helper.TAG, account.name + " disconnected event");
  522. }
  523. @Override
  524. public void closed(ConnectionEvent e) {
  525. Log.e(Helper.TAG, account.name + " closed event");
  526. }
  527. });
  528. // Initiate connection
  529. Log.i(Helper.TAG, account.name + " connect");
  530. for (EntityFolder folder : db.folder().getFolders(account.id))
  531. db.folder().setFolderState(folder.id, null);
  532. db.account().setAccountState(account.id, "connecting");
  533. Helper.connect(this, istore, account);
  534. final boolean capIdle = istore.hasCapability("IDLE");
  535. Log.i(Helper.TAG, account.name + " idle=" + capIdle);
  536. db.account().setAccountState(account.id, "connected");
  537. db.account().setAccountError(account.id, null);
  538. EntityLog.log(this, account.name + " connected");
  539. // Update folder list
  540. synchronizeFolders(account, istore, state);
  541. // Synchronize folders
  542. for (final EntityFolder folder : db.folder().getFolders(account.id, true)) {
  543. Log.i(Helper.TAG, account.name + " sync folder " + folder.name);
  544. db.folder().setFolderState(folder.id, "connecting");
  545. final IMAPFolder ifolder = (IMAPFolder) istore.getFolder(folder.name);
  546. try {
  547. ifolder.open(Folder.READ_WRITE);
  548. } catch (Throwable ex) {
  549. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  550. throw ex;
  551. }
  552. folders.put(folder, ifolder);
  553. db.folder().setFolderState(folder.id, "connected");
  554. db.folder().setFolderError(folder.id, null);
  555. // Keep folder connection alive
  556. Thread poller = new Thread(new Runnable() {
  557. @Override
  558. public void run() {
  559. try {
  560. // Process pending operations
  561. processOperations(folder, isession, istore, ifolder);
  562. // Listen for new and deleted messages
  563. ifolder.addMessageCountListener(new MessageCountAdapter() {
  564. @Override
  565. public void messagesAdded(MessageCountEvent e) {
  566. synchronized (lock) {
  567. try {
  568. Log.i(Helper.TAG, folder.name + " messages added");
  569. FetchProfile fp = new FetchProfile();
  570. fp.add(FetchProfile.Item.ENVELOPE);
  571. fp.add(FetchProfile.Item.FLAGS);
  572. fp.add(FetchProfile.Item.CONTENT_INFO); // body structure
  573. fp.add(UIDFolder.FetchProfileItem.UID);
  574. fp.add(IMAPFolder.FetchProfileItem.HEADERS);
  575. fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
  576. fp.add(FetchProfile.Item.SIZE);
  577. fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
  578. ifolder.fetch(e.getMessages(), fp);
  579. for (Message imessage : e.getMessages())
  580. try {
  581. long id;
  582. try {
  583. db.beginTransaction();
  584. id = synchronizeMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, false);
  585. db.setTransactionSuccessful();
  586. } finally {
  587. db.endTransaction();
  588. }
  589. downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) imessage, id);
  590. } catch (MessageRemovedException ex) {
  591. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  592. } catch (IOException ex) {
  593. if (ex.getCause() instanceof MessageRemovedException)
  594. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  595. else
  596. throw ex;
  597. }
  598. EntityOperation.process(ServiceSynchronize.this); // download small attachments
  599. } catch (Throwable ex) {
  600. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  601. reportError(account.name, folder.name, ex);
  602. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  603. synchronized (state) {
  604. state.notifyAll();
  605. }
  606. }
  607. }
  608. }
  609. @Override
  610. public void messagesRemoved(MessageCountEvent e) {
  611. synchronized (lock) {
  612. try {
  613. Log.i(Helper.TAG, folder.name + " messages removed");
  614. for (Message imessage : e.getMessages())
  615. try {
  616. long uid = ifolder.getUID(imessage);
  617. DB db = DB.getInstance(ServiceSynchronize.this);
  618. int count = db.message().deleteMessage(folder.id, uid);
  619. Log.i(Helper.TAG, "Deleted uid=" + uid + " count=" + count);
  620. } catch (MessageRemovedException ex) {
  621. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  622. }
  623. } catch (Throwable ex) {
  624. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  625. reportError(account.name, folder.name, ex);
  626. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  627. synchronized (state) {
  628. state.notifyAll();
  629. }
  630. }
  631. }
  632. }
  633. });
  634. // Fetch e-mail
  635. synchronizeMessages(account, folder, ifolder, state);
  636. // Flags (like "seen") at the remote could be changed while synchronizing
  637. // Listen for changed messages
  638. ifolder.addMessageChangedListener(new MessageChangedListener() {
  639. @Override
  640. public void messageChanged(MessageChangedEvent e) {
  641. synchronized (lock) {
  642. try {
  643. try {
  644. Log.i(Helper.TAG, folder.name + " message changed");
  645. FetchProfile fp = new FetchProfile();
  646. fp.add(UIDFolder.FetchProfileItem.UID);
  647. fp.add(IMAPFolder.FetchProfileItem.FLAGS);
  648. ifolder.fetch(new Message[]{e.getMessage()}, fp);
  649. long id;
  650. try {
  651. db.beginTransaction();
  652. id = synchronizeMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), false);
  653. db.setTransactionSuccessful();
  654. } finally {
  655. db.endTransaction();
  656. }
  657. downloadMessage(ServiceSynchronize.this, folder, ifolder, (IMAPMessage) e.getMessage(), id);
  658. } catch (MessageRemovedException ex) {
  659. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  660. } catch (IOException ex) {
  661. if (ex.getCause() instanceof MessageRemovedException)
  662. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  663. else
  664. throw ex;
  665. }
  666. } catch (Throwable ex) {
  667. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  668. reportError(account.name, folder.name, ex);
  669. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  670. synchronized (state) {
  671. state.notifyAll();
  672. }
  673. }
  674. }
  675. }
  676. });
  677. if (!capIdle) {
  678. Log.i(Helper.TAG, folder.name + " start polling");
  679. while (state.running) {
  680. try {
  681. Thread.sleep((folder.poll_interval == null ? 9 : folder.poll_interval) * 60 * 1000L);
  682. synchronizeMessages(account, folder, ifolder, state);
  683. } catch (InterruptedException ex) {
  684. Log.w(Helper.TAG, folder.name + " poll " + ex.toString());
  685. }
  686. }
  687. }
  688. } catch (Throwable ex) {
  689. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  690. reportError(account.name, folder.name, ex);
  691. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  692. synchronized (state) {
  693. state.notifyAll();
  694. }
  695. } finally {
  696. if (!capIdle)
  697. Log.i(Helper.TAG, folder.name + " end polling");
  698. }
  699. }
  700. }, "sync.poller." + folder.id);
  701. poller.start();
  702. pollers.add(poller);
  703. // Receive folder events
  704. if (capIdle) {
  705. Thread idler = new Thread(new Runnable() {
  706. @Override
  707. public void run() {
  708. try {
  709. Log.i(Helper.TAG, folder.name + " start idle");
  710. while (state.running && ifolder.isOpen()) {
  711. Log.i(Helper.TAG, folder.name + " do idle");
  712. ifolder.idle(false);
  713. //Log.i(Helper.TAG, folder.name + " done idle");
  714. }
  715. } catch (Throwable ex) {
  716. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  717. reportError(account.name, folder.name, ex);
  718. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  719. synchronized (state) {
  720. state.notifyAll();
  721. }
  722. } finally {
  723. Log.i(Helper.TAG, folder.name + " end idle");
  724. }
  725. }
  726. }, "sync.idle." + folder.id);
  727. idler.start();
  728. idlers.add(idler);
  729. }
  730. }
  731. backoff = CONNECT_BACKOFF_START;
  732. BroadcastReceiver processFolder = new BroadcastReceiver() {
  733. @Override
  734. public void onReceive(Context context, final Intent intent) {
  735. executor.submit(new Runnable() {
  736. @Override
  737. public void run() {
  738. long fid = intent.getLongExtra("folder", -1);
  739. Log.i(Helper.TAG, "Process folder=" + fid + " intent=" + intent);
  740. // Get folder
  741. EntityFolder folder = null;
  742. IMAPFolder ifolder = null;
  743. for (EntityFolder f : folders.keySet())
  744. if (f.id == fid) {
  745. folder = f;
  746. ifolder = folders.get(f);
  747. break;
  748. }
  749. final boolean shouldClose = (ifolder == null);
  750. try {
  751. if (folder == null)
  752. folder = db.folder().getFolder(fid);
  753. Log.i(Helper.TAG, folder.name + " run " + (shouldClose ? "offline" : "online"));
  754. if (ifolder == null) {
  755. // Prevent unnecessary folder connections
  756. if (ACTION_PROCESS_OPERATIONS.equals(intent.getAction()))
  757. if (db.operation().getOperationCount(fid) == 0)
  758. return;
  759. db.folder().setFolderState(folder.id, "connecting");
  760. ifolder = (IMAPFolder) istore.getFolder(folder.name);
  761. ifolder.open(Folder.READ_WRITE);
  762. db.folder().setFolderState(folder.id, "connected");
  763. db.folder().setFolderError(folder.id, null);
  764. }
  765. if (ACTION_PROCESS_OPERATIONS.equals(intent.getAction()))
  766. processOperations(folder, isession, istore, ifolder);
  767. else if (ACTION_SYNCHRONIZE_FOLDER.equals(intent.getAction())) {
  768. processOperations(folder, isession, istore, ifolder);
  769. synchronizeMessages(account, folder, ifolder, state);
  770. }
  771. } catch (Throwable ex) {
  772. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  773. reportError(account.name, folder.name, ex);
  774. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  775. } finally {
  776. if (shouldClose) {
  777. if (ifolder != null && ifolder.isOpen()) {
  778. db.folder().setFolderState(folder.id, "closing");
  779. try {
  780. ifolder.close(false);
  781. } catch (MessagingException ex) {
  782. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  783. }
  784. }
  785. db.folder().setFolderState(folder.id, null);
  786. }
  787. }
  788. }
  789. });
  790. }
  791. };
  792. // Listen for folder operations
  793. IntentFilter f = new IntentFilter();
  794. f.addAction(ACTION_SYNCHRONIZE_FOLDER);
  795. f.addAction(ACTION_PROCESS_OPERATIONS);
  796. f.addDataType("account/" + account.id);
  797. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
  798. lbm.registerReceiver(processFolder, f);
  799. try {
  800. // Keep store alive
  801. while (state.running) {
  802. EntityLog.log(this, account.name + " wait=" + account.poll_interval);
  803. synchronized (state) {
  804. try {
  805. state.wait(account.poll_interval * 60 * 1000L);
  806. } catch (InterruptedException ex) {
  807. Log.w(Helper.TAG, account.name + " wait " + ex.toString());
  808. }
  809. }
  810. if (!istore.isConnected())
  811. throw new StoreClosedException(istore);
  812. for (EntityFolder folder : folders.keySet())
  813. if (!folders.get(folder).isOpen())
  814. throw new FolderClosedException(folders.get(folder));
  815. }
  816. Log.i(Helper.TAG, account.name + " done running=" + state.running);
  817. } finally {
  818. lbm.unregisterReceiver(processFolder);
  819. }
  820. } catch (Throwable ex) {
  821. Log.e(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  822. reportError(account.name, null, ex);
  823. db.account().setAccountError(account.id, Helper.formatThrowable(ex));
  824. } finally {
  825. EntityLog.log(this, account.name + " closing");
  826. db.account().setAccountState(account.id, "closing");
  827. for (EntityFolder folder : folders.keySet())
  828. db.folder().setFolderState(folder.id, "closing");
  829. // Stop pollers
  830. for (Thread poller : pollers) {
  831. poller.interrupt();
  832. join(poller);
  833. }
  834. // Close store
  835. try {
  836. Thread t = new Thread(new Runnable() {
  837. @Override
  838. public void run() {
  839. try {
  840. EntityLog.log(ServiceSynchronize.this, account.name + " store closing");
  841. istore.close();
  842. EntityLog.log(ServiceSynchronize.this, account.name + " store closed");
  843. } catch (Throwable ex) {
  844. Log.w(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  845. }
  846. }
  847. });
  848. t.start();
  849. try {
  850. t.join(MessageHelper.NETWORK_TIMEOUT);
  851. if (t.isAlive())
  852. Log.w(Helper.TAG, account.name + " Close timeout");
  853. } catch (InterruptedException ex) {
  854. Log.w(Helper.TAG, account.name + " close wait " + ex.toString());
  855. t.interrupt();
  856. }
  857. } finally {
  858. EntityLog.log(this, account.name + " closed");
  859. db.account().setAccountState(account.id, null);
  860. for (EntityFolder folder : folders.keySet())
  861. db.folder().setFolderState(folder.id, null);
  862. }
  863. // Stop idlers
  864. for (Thread idler : idlers) {
  865. idler.interrupt();
  866. join(idler);
  867. }
  868. }
  869. if (state.running) {
  870. try {
  871. EntityLog.log(this, account.name + " backoff=" + backoff);
  872. Thread.sleep(backoff * 1000L);
  873. if (backoff < CONNECT_BACKOFF_MAX)
  874. backoff *= 2;
  875. } catch (InterruptedException ex) {
  876. Log.w(Helper.TAG, account.name + " backoff " + ex.toString());
  877. }
  878. }
  879. }
  880. EntityLog.log(this, account.name + " stopped");
  881. }
  882. private void processOperations(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder) throws MessagingException, JSONException, IOException {
  883. synchronized (lock) {
  884. try {
  885. Log.i(Helper.TAG, folder.name + " start process");
  886. DB db = DB.getInstance(this);
  887. List<EntityOperation> ops = db.operation().getOperationsByFolder(folder.id);
  888. Log.i(Helper.TAG, folder.name + " pending operations=" + ops.size());
  889. for (EntityOperation op : ops)
  890. try {
  891. Log.i(Helper.TAG, folder.name +
  892. " start op=" + op.id + "/" + op.name +
  893. " msg=" + op.message +
  894. " args=" + op.args);
  895. EntityMessage message = db.message().getMessage(op.message);
  896. try {
  897. if (message == null)
  898. throw new MessageRemovedException();
  899. db.message().setMessageError(message.id, null);
  900. if (message.uid == null &&
  901. (EntityOperation.SEEN.equals(op.name) ||
  902. EntityOperation.DELETE.equals(op.name) ||
  903. EntityOperation.MOVE.equals(op.name) ||
  904. EntityOperation.HEADERS.equals(op.name)))
  905. throw new IllegalArgumentException(op.name + " without uid");
  906. JSONArray jargs = new JSONArray(op.args);
  907. if (EntityOperation.SEEN.equals(op.name))
  908. doSeen(folder, ifolder, message, jargs, db);
  909. else if (EntityOperation.FLAG.equals(op.name))
  910. doFlag(folder, ifolder, message, jargs, db);
  911. else if (EntityOperation.ADD.equals(op.name))
  912. doAdd(folder, isession, ifolder, message, jargs, db);
  913. else if (EntityOperation.MOVE.equals(op.name))
  914. doMove(folder, isession, istore, ifolder, message, jargs, db);
  915. else if (EntityOperation.DELETE.equals(op.name))
  916. doDelete(folder, ifolder, message, jargs, db);
  917. else if (EntityOperation.SEND.equals(op.name))
  918. doSend(message, db);
  919. else if (EntityOperation.HEADERS.equals(op.name))
  920. doHeaders(folder, ifolder, message, db);
  921. else if (EntityOperation.BODY.equals(op.name))
  922. doBody(folder, ifolder, message, db);
  923. else if (EntityOperation.ATTACHMENT.equals(op.name))
  924. doAttachment(folder, op, ifolder, message, jargs, db);
  925. else
  926. throw new MessagingException("Unknown operation name=" + op.name);
  927. // Operation succeeded
  928. db.operation().deleteOperation(op.id);
  929. } catch (Throwable ex) {
  930. // TODO: SMTP response codes: https://www.ietf.org/rfc/rfc821.txt
  931. if (ex instanceof SendFailedException)
  932. reportError(null, folder.name, ex);
  933. if (message != null)
  934. db.message().setMessageError(message.id, Helper.formatThrowable(ex));
  935. if (ex instanceof MessageRemovedException ||
  936. ex instanceof FolderNotFoundException ||
  937. ex instanceof SendFailedException) {
  938. Log.w(Helper.TAG, "Unrecoverable " + ex + "\n" + Log.getStackTraceString(ex));
  939. // There is no use in repeating
  940. db.operation().deleteOperation(op.id);
  941. continue;
  942. } else if (ex instanceof MessagingException) {
  943. // Socket timeout is a recoverable condition (send message)
  944. if (ex.getCause() instanceof SocketTimeoutException) {
  945. Log.w(Helper.TAG, "Recoverable " + ex + "\n" + Log.getStackTraceString(ex));
  946. // No need to inform user
  947. return;
  948. }
  949. }
  950. throw ex;
  951. }
  952. } finally {
  953. Log.i(Helper.TAG, folder.name + " end op=" + op.id + "/" + op.name);
  954. }
  955. } finally {
  956. Log.i(Helper.TAG, folder.name + " end process");
  957. }
  958. }
  959. }
  960. private void doSeen(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
  961. // Mark message (un)seen
  962. boolean seen = jargs.getBoolean(0);
  963. if (message.seen == seen)
  964. return;
  965. Message imessage = ifolder.getMessageByUID(message.uid);
  966. if (imessage == null)
  967. throw new MessageRemovedException();
  968. imessage.setFlag(Flags.Flag.SEEN, seen);
  969. db.message().setMessageSeen(message.id, seen);
  970. }
  971. private void doFlag(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
  972. // Star/unstar message
  973. boolean flagged = jargs.getBoolean(0);
  974. Message imessage = ifolder.getMessageByUID(message.uid);
  975. if (imessage == null)
  976. throw new MessageRemovedException();
  977. imessage.setFlag(Flags.Flag.FLAGGED, flagged);
  978. db.message().setMessageFlagged(message.id, flagged);
  979. }
  980. private void doAdd(EntityFolder folder, Session isession, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException, IOException {
  981. // Append message
  982. List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
  983. MimeMessage imessage = MessageHelper.from(this, message, null, attachments, isession);
  984. AppendUID[] uid = ifolder.appendUIDMessages(new Message[]{imessage});
  985. db.message().setMessageUid(message.id, uid[0].uid);
  986. Log.i(Helper.TAG, "Appended uid=" + uid[0].uid);
  987. if (message.uid != null) {
  988. Message iprev = ifolder.getMessageByUID(message.uid);
  989. if (iprev != null) {
  990. Log.i(Helper.TAG, "Deleting existing uid=" + message.uid);
  991. iprev.setFlag(Flags.Flag.DELETED, true);
  992. ifolder.expunge();
  993. }
  994. }
  995. }
  996. private void doMove(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws JSONException, MessagingException, IOException {
  997. // Move message
  998. long id = jargs.getLong(0);
  999. EntityFolder target = db.folder().getFolder(id);
  1000. if (target == null)
  1001. throw new FolderNotFoundException();
  1002. // Get message
  1003. Message imessage = ifolder.getMessageByUID(message.uid);
  1004. if (imessage == null)
  1005. throw new MessageRemovedException();
  1006. if (istore.hasCapability("MOVE")) {
  1007. Folder itarget = istore.getFolder(target.name);
  1008. ifolder.moveMessages(new Message[]{imessage}, itarget);
  1009. } else {
  1010. Log.w(Helper.TAG, "MOVE by DELETE/APPEND");
  1011. List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
  1012. if (!EntityFolder.ARCHIVE.equals(folder.type)) {
  1013. imessage.setFlag(Flags.Flag.DELETED, true);
  1014. ifolder.expunge();
  1015. }
  1016. MimeMessageEx icopy = MessageHelper.from(this, message, null, attachments, isession);
  1017. Folder itarget = istore.getFolder(target.name);
  1018. itarget.appendMessages(new Message[]{icopy});
  1019. }
  1020. }
  1021. private void doDelete(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
  1022. // Delete message
  1023. Message imessage = ifolder.getMessageByUID(message.uid);
  1024. if (imessage == null)
  1025. throw new MessageRemovedException();
  1026. imessage.setFlag(Flags.Flag.DELETED, true);
  1027. ifolder.expunge();
  1028. db.message().deleteMessage(message.id);
  1029. }
  1030. private void doSend(EntityMessage message, DB db) throws MessagingException, IOException {
  1031. // Send message
  1032. EntityIdentity ident = db.identity().getIdentity(message.identity);
  1033. if (!ident.synchronize) {
  1034. // Message will remain in outbox
  1035. return;
  1036. }
  1037. // Create session
  1038. Properties props = MessageHelper.getSessionProperties(ident.auth_type);
  1039. final Session isession = Session.getInstance(props, null);
  1040. // Create message
  1041. MimeMessage imessage;
  1042. EntityMessage reply = (message.replying == null ? null : db.message().getMessage(message.replying));
  1043. List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
  1044. imessage = MessageHelper.from(this, message, reply, attachments, isession);
  1045. if (ident.replyto != null)
  1046. imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
  1047. // Create transport
  1048. // TODO: cache transport?
  1049. Transport itransport = isession.getTransport(ident.starttls ? "smtp" : "smtps");
  1050. try {
  1051. // Connect transport
  1052. db.identity().setIdentityState(ident.id, "connecting");
  1053. try {
  1054. itransport.connect(ident.host, ident.port, ident.user, ident.password);
  1055. } catch (AuthenticationFailedException ex) {
  1056. if (ident.auth_type == Helper.AUTH_TYPE_GMAIL) {
  1057. EntityAccount account = db.account().getAccount(ident.account);
  1058. ident.password = Helper.refreshToken(this, "com.google", ident.user, account.password);
  1059. DB.getInstance(this).identity().setIdentityPassword(ident.id, ident.password);
  1060. itransport.connect(ident.host, ident.port, ident.user, ident.password);
  1061. } else
  1062. throw ex;
  1063. }
  1064. db.identity().setIdentityState(ident.id, "connected");
  1065. db.identity().setIdentityError(ident.id, null);
  1066. // Send message
  1067. Address[] to = imessage.getAllRecipients();
  1068. itransport.sendMessage(imessage, to);
  1069. Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user +
  1070. " to " + TextUtils.join(", ", to));
  1071. try {
  1072. db.beginTransaction();
  1073. // Mark message as sent
  1074. // - will be moved to sent folder by synchronize message later
  1075. message.sent = imessage.getSentDate().getTime();
  1076. message.seen = true;
  1077. message.ui_seen = true;
  1078. db.message().updateMessage(message);
  1079. if (ident.store_sent) {
  1080. EntityFolder sent = db.folder().getFolderByType(ident.account, EntityFolder.SENT);
  1081. if (sent != null) {
  1082. message.folder = sent.id;
  1083. message.uid = null;
  1084. db.message().updateMessage(message);
  1085. Log.i(Helper.TAG, "Appending sent msgid=" + message.msgid);
  1086. EntityOperation.queue(db, message, EntityOperation.ADD); // Could already exist
  1087. }
  1088. }
  1089. db.setTransactionSuccessful();
  1090. } finally {
  1091. db.endTransaction();
  1092. }
  1093. EntityOperation.process(this);
  1094. } catch (MessagingException ex) {
  1095. db.identity().setIdentityError(ident.id, Helper.formatThrowable(ex));
  1096. throw ex;
  1097. } finally {
  1098. try {
  1099. itransport.close();
  1100. } finally {
  1101. db.identity().setIdentityState(ident.id, null);
  1102. }
  1103. }
  1104. }
  1105. private void doHeaders(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, DB db) throws MessagingException {
  1106. Message imessage = ifolder.getMessageByUID(message.uid);
  1107. if (imessage == null)
  1108. throw new MessageRemovedException();
  1109. Enumeration<Header> headers = imessage.getAllHeaders();
  1110. StringBuilder sb = new StringBuilder();
  1111. while (headers.hasMoreElements()) {
  1112. Header header = headers.nextElement();
  1113. sb.append(header.getName()).append(": ").append(header.getValue()).append("\n");
  1114. }
  1115. db.message().setMessageHeaders(message.id, sb.toString());
  1116. }
  1117. private void doBody(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, DB db) throws MessagingException, IOException {
  1118. // Download message body
  1119. if (message.content)
  1120. return;
  1121. // Get message
  1122. Message imessage = ifolder.getMessageByUID(message.uid);
  1123. if (imessage == null)
  1124. throw new MessageRemovedException();
  1125. MessageHelper helper = new MessageHelper((MimeMessage) imessage);
  1126. message.write(this, helper.getHtml());
  1127. db.message().setMessageContent(message.id, true);
  1128. }
  1129. private void doAttachment(EntityFolder folder, EntityOperation op, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws JSONException, MessagingException, IOException {
  1130. // Download attachment
  1131. int sequence = jargs.getInt(0);
  1132. // Get attachment
  1133. EntityAttachment attachment = db.attachment().getAttachment(op.message, sequence);
  1134. if (attachment.available)
  1135. return;
  1136. // Get message
  1137. Message imessage = ifolder.getMessageByUID(message.uid);
  1138. if (imessage == null)
  1139. throw new MessageRemovedException();
  1140. // Download attachment
  1141. MessageHelper helper = new MessageHelper((MimeMessage) imessage);
  1142. EntityAttachment a = helper.getAttachments().get(sequence - 1);
  1143. attachment.part = a.part;
  1144. attachment.download(this, db);
  1145. }
  1146. private void synchronizeFolders(EntityAccount account, IMAPStore istore, ServiceState state) throws MessagingException {
  1147. DB db = DB.getInstance(this);
  1148. try {
  1149. db.beginTransaction();
  1150. Log.v(Helper.TAG, "Start sync folders");
  1151. List<String> names = new ArrayList<>();
  1152. for (EntityFolder folder : db.folder().getUserFolders(account.id))
  1153. names.add(folder.name);
  1154. Log.i(Helper.TAG, "Local folder count=" + names.size());
  1155. Folder[] ifolders = istore.getDefaultFolder().list("*"); // TODO: is the pattern correct?
  1156. Log.i(Helper.TAG, "Remote folder count=" + ifolders.length);
  1157. for (Folder ifolder : ifolders) {
  1158. String[] attrs = ((IMAPFolder) ifolder).getAttributes();
  1159. boolean system = false;
  1160. boolean selectable = true;
  1161. for (String attr : attrs) {
  1162. if ("\\Noselect".equals(attr)) { // TODO: is this attribute correct?
  1163. selectable = false;
  1164. break;
  1165. }
  1166. if (attr.startsWith("\\")) {
  1167. attr = attr.substring(1);
  1168. if (EntityFolder.SYSTEM_FOLDER_ATTR.contains(attr)) {
  1169. int index = EntityFolder.SYSTEM_FOLDER_ATTR.indexOf(attr);
  1170. system = EntityFolder.SYSTEM.equals(EntityFolder.SYSTEM_FOLDER_TYPE.get(index));
  1171. if (!system)
  1172. selectable = false;
  1173. break;
  1174. }
  1175. }
  1176. }
  1177. if (selectable) {
  1178. Log.i(Helper.TAG, ifolder.getFullName() + " candidate attr=" + TextUtils.join(",", attrs));
  1179. EntityFolder folder = db.folder().getFolderByName(account.id, ifolder.getFullName());
  1180. if (folder == null) {
  1181. folder = new EntityFolder();
  1182. folder.account = account.id;
  1183. folder.name = ifolder.getFullName();
  1184. folder.type = (system ? EntityFolder.SYSTEM : EntityFolder.USER);
  1185. folder.synchronize = false;
  1186. folder.after = EntityFolder.DEFAULT_USER_SYNC;
  1187. db.folder().insertFolder(folder);
  1188. Log.i(Helper.TAG, folder.name + " added");
  1189. } else {
  1190. if (system)
  1191. db.folder().setFolderType(folder.id, EntityFolder.SYSTEM);
  1192. names.remove(folder.name);
  1193. }
  1194. }
  1195. }
  1196. Log.i(Helper.TAG, "Delete local folder=" + names.size());
  1197. for (String name : names)
  1198. db.folder().deleteFolder(account.id, name);
  1199. db.setTransactionSuccessful();
  1200. } finally {
  1201. db.endTransaction();
  1202. Log.v(Helper.TAG, "End sync folder");
  1203. }
  1204. }
  1205. private void synchronizeMessages(EntityAccount account, EntityFolder folder, IMAPFolder ifolder, ServiceState state) throws MessagingException, IOException {
  1206. DB db = DB.getInstance(this);
  1207. try {
  1208. Log.v(Helper.TAG, folder.name + " start sync after=" + folder.after);
  1209. db.folder().setFolderState(folder.id, "syncing");
  1210. // Get reference times
  1211. Calendar cal = Calendar.getInstance();
  1212. cal.add(Calendar.DAY_OF_MONTH, -folder.after);
  1213. cal.set(Calendar.HOUR_OF_DAY, 0);
  1214. cal.set(Calendar.MINUTE, 0);
  1215. cal.set(Calendar.SECOND, 0);
  1216. cal.set(Calendar.MILLISECOND, 0);
  1217. long ago = cal.getTimeInMillis();
  1218. if (ago < 0)
  1219. ago = 0;
  1220. Log.i(Helper.TAG, folder.name + " ago=" + new Date(ago));
  1221. // Delete old local messages
  1222. int old = db.message().deleteMessagesBefore(folder.id, ago);
  1223. Log.i(Helper.TAG, folder.name + " local old=" + old);
  1224. // Get list of local uids
  1225. List<Long> uids = db.message().getUids(folder.id, ago);
  1226. Log.i(Helper.TAG, folder.name + " local count=" + uids.size());
  1227. // Reduce list of local uids
  1228. long search = SystemClock.elapsedRealtime();
  1229. Message[] imessages = ifolder.search(new ReceivedDateTerm(ComparisonTerm.GE, new Date(ago)));
  1230. Log.i(Helper.TAG, folder.name + " remote count=" + imessages.length +
  1231. " search=" + (SystemClock.elapsedRealtime() - search) + " ms");
  1232. FetchProfile fp = new FetchProfile();
  1233. fp.add(UIDFolder.FetchProfileItem.UID);
  1234. fp.add(FetchProfile.Item.FLAGS);
  1235. ifolder.fetch(imessages, fp);
  1236. long fetch = SystemClock.elapsedRealtime();
  1237. Log.i(Helper.TAG, folder.name + " remote fetched=" + (SystemClock.elapsedRealtime() - fetch) + " ms");
  1238. for (Message imessage : imessages) {
  1239. if (!state.running)
  1240. return;
  1241. try {
  1242. uids.remove(ifolder.getUID(imessage));
  1243. } catch (MessageRemovedException ex) {
  1244. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  1245. } catch (Throwable ex) {
  1246. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  1247. reportError(account.name, folder.name, ex);
  1248. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  1249. }
  1250. }
  1251. // Delete local messages not at remote
  1252. Log.i(Helper.TAG, folder.name + " delete=" + uids.size());
  1253. for (Long uid : uids) {
  1254. int count = db.message().deleteMessage(folder.id, uid);
  1255. Log.i(Helper.TAG, folder.name + " delete local uid=" + uid + " count=" + count);
  1256. }
  1257. fp.add(FetchProfile.Item.ENVELOPE);
  1258. // fp.add(FetchProfile.Item.FLAGS);
  1259. fp.add(FetchProfile.Item.CONTENT_INFO); // body structure
  1260. // fp.add(UIDFolder.FetchProfileItem.UID);
  1261. fp.add(IMAPFolder.FetchProfileItem.HEADERS);
  1262. // fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
  1263. fp.add(FetchProfile.Item.SIZE);
  1264. fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
  1265. // Add/update local messages
  1266. Long[] ids = new Long[imessages.length];
  1267. Log.i(Helper.TAG, folder.name + " add=" + imessages.length);
  1268. for (int i = imessages.length - 1; i >= 0; i -= SYNC_BATCH_SIZE) {
  1269. int from = Math.max(0, i - SYNC_BATCH_SIZE + 1);
  1270. //Log.i(Helper.TAG, folder.name + " update " + from + " .. " + i);
  1271. Message[] isub = Arrays.copyOfRange(imessages, from, i + 1);
  1272. // Full fetch new/changed messages only
  1273. List<Message> full = new ArrayList<>();
  1274. for (Message imessage : isub) {
  1275. long uid = ifolder.getUID(imessage);
  1276. EntityMessage message = db.message().getMessageByUid(folder.id, uid);
  1277. if (message == null)
  1278. full.add(imessage);
  1279. }
  1280. if (full.size() > 0) {
  1281. long headers = SystemClock.elapsedRealtime();
  1282. ifolder.fetch(full.toArray(new Message[0]), fp);
  1283. Log.i(Helper.TAG, folder.name + " fetched headers=" + full.size() +
  1284. " " + (SystemClock.elapsedRealtime() - headers) + " ms");
  1285. }
  1286. for (int j = isub.length - 1; j >= 0; j--)
  1287. try {
  1288. db.beginTransaction();
  1289. ids[from + j] = synchronizeMessage(this, folder, ifolder, (IMAPMessage) isub[j], false);
  1290. db.setTransactionSuccessful();
  1291. Thread.sleep(20);
  1292. } catch (MessageRemovedException ex) {
  1293. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  1294. } catch (FolderClosedException ex) {
  1295. throw ex;
  1296. } catch (FolderClosedIOException ex) {
  1297. throw ex;
  1298. } catch (Throwable ex) {
  1299. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  1300. } finally {
  1301. db.endTransaction();
  1302. // Reduce memory usage
  1303. ((IMAPMessage) isub[j]).invalidateHeaders();
  1304. }
  1305. try {
  1306. Thread.sleep(100);
  1307. } catch (InterruptedException ignored) {
  1308. }
  1309. }
  1310. db.folder().setFolderState(folder.id, "downloading");
  1311. //fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
  1312. // Download messages/attachments
  1313. Log.i(Helper.TAG, folder.name + " download=" + imessages.length);
  1314. for (int i = imessages.length - 1; i >= 0; i -= DOWNLOAD_BATCH_SIZE) {
  1315. int from = Math.max(0, i - DOWNLOAD_BATCH_SIZE + 1);
  1316. //Log.i(Helper.TAG, folder.name + " download " + from + " .. " + i);
  1317. Message[] isub = Arrays.copyOfRange(imessages, from, i + 1);
  1318. // Fetch on demand
  1319. for (int j = isub.length - 1; j >= 0; j--)
  1320. try {
  1321. //Log.i(Helper.TAG, folder.name + " download index=" + (from + j) + " id=" + ids[from + j]);
  1322. if (ids[from + j] != null) {
  1323. downloadMessage(this, folder, ifolder, (IMAPMessage) isub[j], ids[from + j]);
  1324. Thread.sleep(20);
  1325. }
  1326. } catch (FolderClosedException ex) {
  1327. throw ex;
  1328. } catch (FolderClosedIOException ex) {
  1329. throw ex;
  1330. } catch (Throwable ex) {
  1331. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  1332. } finally {
  1333. // Free memory
  1334. ((IMAPMessage) isub[j]).invalidateHeaders();
  1335. }
  1336. try {
  1337. Thread.sleep(100);
  1338. } catch (InterruptedException ignored) {
  1339. }
  1340. }
  1341. } finally {
  1342. Log.v(Helper.TAG, folder.name + " end sync");
  1343. db.folder().setFolderState(folder.id, ifolder.isOpen() ? "connected" : "disconnected");
  1344. }
  1345. }
  1346. static Long synchronizeMessage(Context context, EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage, boolean found) throws MessagingException, IOException {
  1347. long uid = ifolder.getUID(imessage);
  1348. if (imessage.isExpunged()) {
  1349. Log.i(Helper.TAG, folder.name + " expunged uid=" + uid);
  1350. throw new MessageRemovedException();
  1351. }
  1352. if (imessage.isSet(Flags.Flag.DELETED)) {
  1353. Log.i(Helper.TAG, folder.name + " deleted uid=" + uid);
  1354. throw new MessageRemovedException();
  1355. }
  1356. MessageHelper helper = new MessageHelper(imessage);
  1357. boolean seen = helper.getSeen();
  1358. boolean flagged = helper.getFlagged();
  1359. DB db = DB.getInstance(context);
  1360. // Find message by uid (fast, no headers required)
  1361. EntityMessage message = db.message().getMessageByUid(folder.id, uid);
  1362. // Find message by Message-ID (slow, headers required)
  1363. // - messages in inbox have same id as message sent to self
  1364. // - messages in archive have same id as original
  1365. if (message == null) {
  1366. // Will fetch headers within database transaction
  1367. String msgid = helper.getMessageID();
  1368. String[] refs = helper.getReferences();
  1369. String reference = (refs.length == 1 && refs[0].indexOf(BuildConfig.APPLICATION_ID) > 0 ? refs[0] : msgid);
  1370. Log.i(Helper.TAG, "Searching for " + msgid + " / " + reference);
  1371. for (EntityMessage dup : db.message().getMessageByMsgId(folder.account, msgid, reference)) {
  1372. EntityFolder dfolder = db.folder().getFolder(dup.folder);
  1373. boolean outbox = EntityFolder.OUTBOX.equals(dfolder.type);
  1374. Log.i(Helper.TAG, folder.name + " found as id=" + dup.id +
  1375. " folder=" + dfolder.type + ":" + dup.folder + "/" + folder.type + ":" + folder.id);
  1376. if (dup.folder.equals(folder.id) || outbox) {
  1377. Log.i(Helper.TAG, folder.name + " found as id=" + dup.id + " uid=" + dup.uid + " msgid=" + msgid);
  1378. dup.folder = folder.id;
  1379. dup.uid = uid;
  1380. if (TextUtils.isEmpty(dup.thread)) // outbox: only now the uid is known
  1381. dup.thread = helper.getThreadId(uid);
  1382. db.message().updateMessage(dup);
  1383. message = dup;
  1384. }
  1385. }
  1386. }
  1387. if (message == null) {
  1388. message = new EntityMessage();
  1389. message.account = folder.account;
  1390. message.folder = folder.id;
  1391. message.uid = uid;
  1392. if (!EntityFolder.ARCHIVE.equals(folder.type)) {
  1393. message.msgid = helper.getMessageID();
  1394. if (TextUtils.isEmpty(message.msgid))
  1395. Log.w(Helper.TAG, "No Message-ID id=" + message.id + " uid=" + message.uid);
  1396. }
  1397. message.references = TextUtils.join(" ", helper.getReferences());
  1398. message.inreplyto = helper.getInReplyTo();
  1399. message.deliveredto = helper.getDeliveredTo();
  1400. message.thread = helper.getThreadId(uid);
  1401. message.from = helper.getFrom();
  1402. message.to = helper.getTo();
  1403. message.cc = helper.getCc();
  1404. message.bcc = helper.getBcc();
  1405. message.reply = helper.getReply();
  1406. message.subject = imessage.getSubject();
  1407. message.size = helper.getSize();
  1408. message.content = false;
  1409. message.received = imessage.getReceivedDate().getTime();
  1410. message.sent = (imessage.getSentDate() == null ? null : imessage.getSentDate().getTime());
  1411. message.seen = seen;
  1412. message.ui_seen = seen;
  1413. message.flagged = false;
  1414. message.ui_flagged = false;
  1415. message.ui_hide = false;
  1416. message.ui_found = found;
  1417. if (ContextCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
  1418. == PackageManager.PERMISSION_GRANTED) {
  1419. try {
  1420. if (message.from != null)
  1421. for (int i = 0; i < message.from.length; i++) {
  1422. String email = ((InternetAddress) message.from[i]).getAddress();
  1423. Cursor cursor = null;
  1424. try {
  1425. ContentResolver resolver = context.getContentResolver();
  1426. cursor = resolver.query(ContactsContract.CommonDataKinds.Email.CONTENT_URI,
  1427. new String[]{
  1428. ContactsContract.CommonDataKinds.Photo.CONTACT_ID,
  1429. ContactsContract.Contacts.DISPLAY_NAME
  1430. },
  1431. ContactsContract.CommonDataKinds.Email.ADDRESS + " = ?",
  1432. new String[]{email}, null);
  1433. if (cursor.moveToNext()) {
  1434. int colContactId = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Photo.CONTACT_ID);
  1435. int colDisplayName = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
  1436. long contactId = cursor.getLong(colContactId);
  1437. String displayName = cursor.getString(colDisplayName);
  1438. Uri uri = ContentUris.withAppendedId(ContactsContract.Contacts.CONTENT_URI, contactId);
  1439. message.avatar = uri.toString();
  1440. if (!TextUtils.isEmpty(displayName))
  1441. ((InternetAddress) message.from[i]).setPersonal(displayName);
  1442. }
  1443. } finally {
  1444. if (cursor != null)
  1445. cursor.close();
  1446. }
  1447. }
  1448. } catch (Throwable ex) {
  1449. Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  1450. }
  1451. }
  1452. message.id = db.message().insertMessage(message);
  1453. Log.i(Helper.TAG, folder.name + " added id=" + message.id + " uid=" + message.uid);
  1454. int sequence = 1;
  1455. for (EntityAttachment attachment : helper.getAttachments()) {
  1456. Log.i(Helper.TAG, folder.name + " attachment seq=" + sequence +
  1457. " name=" + attachment.name + " type=" + attachment.type + " cid=" + attachment.cid);
  1458. if (!TextUtils.isEmpty(attachment.cid) &&
  1459. db.attachment().getAttachment(message.id, attachment.cid) != null) {
  1460. Log.i(Helper.TAG, "Skipping duplicated CID");
  1461. continue;
  1462. }
  1463. attachment.message = message.id;
  1464. attachment.sequence = sequence++;
  1465. attachment.id = db.attachment().insertAttachment(attachment);
  1466. if (message.size != null && attachment.size != null)
  1467. message.size -= attachment.size;
  1468. }
  1469. db.message().updateMessage(message);
  1470. } else {
  1471. if (message.seen != seen || message.seen != message.ui_seen) {
  1472. message.seen = seen;
  1473. message.ui_seen = seen;
  1474. db.message().updateMessage(message);
  1475. Log.i(Helper.TAG, folder.name + " updated id=" + message.id + " uid=" + message.uid + " seen=" + seen);
  1476. }
  1477. if (message.flagged != flagged || message.flagged != message.ui_flagged) {
  1478. message.flagged = flagged;
  1479. message.ui_flagged = flagged;
  1480. db.message().updateMessage(message);
  1481. Log.i(Helper.TAG, folder.name + " updated id=" + message.id + " uid=" + message.uid + " flagged=" + flagged);
  1482. }
  1483. if (message.ui_hide) {
  1484. message.ui_hide = false;
  1485. db.message().updateMessage(message);
  1486. Log.i(Helper.TAG, folder.name + " unhidden id=" + message.id + " uid=" + message.uid);
  1487. }
  1488. }
  1489. return message.id;
  1490. }
  1491. private static void downloadMessage(Context context, EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage, long id) throws MessagingException, IOException {
  1492. DB db = DB.getInstance(context);
  1493. EntityMessage message = db.message().getMessage(id);
  1494. List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
  1495. MessageHelper helper = new MessageHelper(imessage);
  1496. ConnectivityManager cm = context.getSystemService(ConnectivityManager.class);
  1497. boolean metered = (cm == null || cm.isActiveNetworkMetered());
  1498. boolean fetch = false;
  1499. if (!message.content)
  1500. if (!metered || (message.size != null && message.size < MESSAGE_AUTO_DOWNLOAD_SIZE))
  1501. fetch = true;
  1502. if (!fetch)
  1503. for (EntityAttachment attachment : attachments)
  1504. if (!attachment.available)
  1505. if (!metered || (attachment.size != null && attachment.size < ATTACHMENT_AUTO_DOWNLOAD_SIZE)) {
  1506. fetch = true;
  1507. break;
  1508. }
  1509. if (fetch) {
  1510. Log.i(Helper.TAG, folder.name + " fetching message id=" + message.id);
  1511. FetchProfile fp = new FetchProfile();
  1512. fp.add(FetchProfile.Item.ENVELOPE);
  1513. fp.add(FetchProfile.Item.FLAGS);
  1514. fp.add(FetchProfile.Item.CONTENT_INFO); // body structure
  1515. fp.add(UIDFolder.FetchProfileItem.UID);
  1516. fp.add(IMAPFolder.FetchProfileItem.HEADERS);
  1517. fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
  1518. fp.add(FetchProfile.Item.SIZE);
  1519. fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
  1520. ifolder.fetch(new Message[]{imessage}, fp);
  1521. }
  1522. if (!message.content)
  1523. if (!metered || (message.size != null && message.size < MESSAGE_AUTO_DOWNLOAD_SIZE)) {
  1524. message.write(context, helper.getHtml());
  1525. db.message().setMessageContent(message.id, true);
  1526. Log.i(Helper.TAG, folder.name + " downloaded message id=" + message.id + " size=" + message.size);
  1527. }
  1528. List<EntityAttachment> iattachments = null;
  1529. for (int i = 0; i < attachments.size(); i++) {
  1530. EntityAttachment attachment = attachments.get(i);
  1531. if (!attachment.available)
  1532. if (!metered || (attachment.size != null && attachment.size < ATTACHMENT_AUTO_DOWNLOAD_SIZE)) {
  1533. if (iattachments == null)
  1534. iattachments = helper.getAttachments();
  1535. attachment.part = iattachments.get(i).part;
  1536. attachment.download(context, db);
  1537. Log.i(Helper.TAG, folder.name + " downloaded message id=" + message.id + " attachment=" + attachment.name + " size=" + message.size);
  1538. }
  1539. }
  1540. }
  1541. private class ServiceManager extends ConnectivityManager.NetworkCallback {
  1542. private ServiceState state;
  1543. private boolean running = false;
  1544. private long lastLost = 0;
  1545. private Thread main;
  1546. private EntityFolder outbox = null;
  1547. private ExecutorService lifecycle = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
  1548. private ExecutorService executor = Executors.newSingleThreadExecutor(Helper.backgroundThreadFactory);
  1549. @Override
  1550. public void onAvailable(Network network) {
  1551. ConnectivityManager cm = getSystemService(ConnectivityManager.class);
  1552. NetworkInfo ni = cm.getNetworkInfo(network);
  1553. EntityLog.log(ServiceSynchronize.this, "Network available " + network + " running=" + running + " " + ni);
  1554. if (!running) {
  1555. running = true;
  1556. lifecycle.submit(new Runnable() {
  1557. @Override
  1558. public void run() {
  1559. Log.i(Helper.TAG, "Starting service");
  1560. start();
  1561. }
  1562. });
  1563. }
  1564. }
  1565. @Override
  1566. public void onLost(Network network) {
  1567. EntityLog.log(ServiceSynchronize.this, "Network lost " + network + " running=" + running);
  1568. if (running) {
  1569. ConnectivityManager cm = getSystemService(ConnectivityManager.class);
  1570. NetworkInfo ani = (network == null ? null : cm.getActiveNetworkInfo());
  1571. EntityLog.log(ServiceSynchronize.this, "Network active=" + (ani == null ? null : ani.toString()));
  1572. if (ani == null || !ani.isConnected()) {
  1573. EntityLog.log(ServiceSynchronize.this, "Network disconnected=" + ani);
  1574. running = false;
  1575. lastLost = new Date().getTime();
  1576. lifecycle.submit(new Runnable() {
  1577. @Override
  1578. public void run() {
  1579. stop();
  1580. }
  1581. });
  1582. }
  1583. }
  1584. }
  1585. private void start() {
  1586. EntityLog.log(ServiceSynchronize.this, "Main start");
  1587. state = new ServiceState();
  1588. main = new Thread(new Runnable() {
  1589. private Map<Thread, ServiceState> threadState = new HashMap<>();
  1590. @Override
  1591. public void run() {
  1592. try {
  1593. DB db = DB.getInstance(ServiceSynchronize.this);
  1594. outbox = db.folder().getOutbox();
  1595. if (outbox == null) {
  1596. EntityLog.log(ServiceSynchronize.this, "No outbox, halt");
  1597. stopSelf();
  1598. return;
  1599. }
  1600. List<EntityAccount> accounts = db.account().getAccounts(true);
  1601. if (accounts.size() == 0) {
  1602. EntityLog.log(ServiceSynchronize.this, "No accounts, halt");
  1603. stopSelf();
  1604. return;
  1605. }
  1606. long ago = new Date().getTime() - lastLost;
  1607. if (ago < RECONNECT_BACKOFF)
  1608. try {
  1609. long backoff = RECONNECT_BACKOFF - ago;
  1610. EntityLog.log(ServiceSynchronize.this, "Main backoff=" + (backoff / 1000));
  1611. Thread.sleep(backoff);
  1612. } catch (InterruptedException ex) {
  1613. Log.w(Helper.TAG, "main backoff " + ex.toString());
  1614. return;
  1615. }
  1616. // Start monitoring outbox
  1617. IntentFilter f = new IntentFilter();
  1618. f.addAction(ACTION_SYNCHRONIZE_FOLDER);
  1619. f.addAction(ACTION_PROCESS_OPERATIONS);
  1620. f.addDataType("account/outbox");
  1621. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
  1622. lbm.registerReceiver(outboxReceiver, f);
  1623. db.folder().setFolderState(outbox.id, "connected");
  1624. db.folder().setFolderError(outbox.id, null);
  1625. lbm.sendBroadcast(new Intent(ACTION_PROCESS_OPERATIONS)
  1626. .setType("account/outbox")
  1627. .putExtra("folder", outbox.id));
  1628. // Start monitoring accounts
  1629. for (final EntityAccount account : accounts) {
  1630. Log.i(Helper.TAG, account.host + "/" + account.user + " run");
  1631. final ServiceState astate = new ServiceState();
  1632. Thread t = new Thread(new Runnable() {
  1633. @Override
  1634. public void run() {
  1635. try {
  1636. monitorAccount(account, astate);
  1637. } catch (Throwable ex) {
  1638. // Fall-safe
  1639. Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  1640. }
  1641. }
  1642. }, "sync.account." + account.id);
  1643. t.start();
  1644. threadState.put(t, astate);
  1645. }
  1646. EntityLog.log(ServiceSynchronize.this, "Main started");
  1647. synchronized (state) {
  1648. try {
  1649. if (state.running)
  1650. state.wait();
  1651. } catch (InterruptedException ex) {
  1652. Log.w(Helper.TAG, "main wait " + ex.toString());
  1653. }
  1654. }
  1655. // Stop monitoring accounts
  1656. for (Thread t : threadState.keySet()) {
  1657. ServiceState astate = threadState.get(t);
  1658. synchronized (astate) {
  1659. astate.running = false;
  1660. astate.notifyAll();
  1661. }
  1662. t.interrupt();
  1663. join(t);
  1664. }
  1665. threadState.clear();
  1666. // Stop monitoring outbox
  1667. lbm.unregisterReceiver(outboxReceiver);
  1668. Log.i(Helper.TAG, outbox.name + " unlisten operations");
  1669. db.folder().setFolderState(outbox.id, null);
  1670. EntityLog.log(ServiceSynchronize.this, "Main exited");
  1671. } catch (Throwable ex) {
  1672. // Fail-safe
  1673. Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  1674. }
  1675. }
  1676. }, "sync.main");
  1677. main.setPriority(THREAD_PRIORITY_BACKGROUND); // will be inherited
  1678. main.start();
  1679. }
  1680. private void stop() {
  1681. EntityLog.log(ServiceSynchronize.this, "Main stop");
  1682. synchronized (state) {
  1683. state.running = false;
  1684. state.notifyAll();
  1685. }
  1686. // stop wait or backoff
  1687. main.interrupt();
  1688. join(main);
  1689. EntityLog.log(ServiceSynchronize.this, "Main stopped");
  1690. main = null;
  1691. state = null;
  1692. }
  1693. private void restart() {
  1694. if (running)
  1695. lifecycle.submit(new Runnable() {
  1696. @Override
  1697. public void run() {
  1698. stop();
  1699. start();
  1700. }
  1701. });
  1702. }
  1703. private BroadcastReceiver outboxReceiver = new BroadcastReceiver() {
  1704. @Override
  1705. public void onReceive(final Context context, Intent intent) {
  1706. Log.v(Helper.TAG, outbox.name + " run operations");
  1707. executor.submit(new Runnable() {
  1708. @Override
  1709. public void run() {
  1710. DB db = DB.getInstance(context);
  1711. try {
  1712. Log.i(Helper.TAG, outbox.name + " start operations");
  1713. db.folder().setFolderState(outbox.id, "syncing");
  1714. processOperations(outbox, null, null, null);
  1715. } catch (Throwable ex) {
  1716. Log.e(Helper.TAG, outbox.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  1717. reportError(null, outbox.name, ex);
  1718. db.folder().setFolderError(outbox.id, Helper.formatThrowable(ex));
  1719. } finally {
  1720. Log.i(Helper.TAG, outbox.name + " end operations");
  1721. db.folder().setFolderState(outbox.id, null);
  1722. }
  1723. }
  1724. });
  1725. }
  1726. };
  1727. }
  1728. private void join(Thread thread) {
  1729. boolean joined = false;
  1730. while (!joined)
  1731. try {
  1732. Log.i(Helper.TAG, "Joining " + thread.getName());
  1733. thread.join();
  1734. joined = true;
  1735. Log.i(Helper.TAG, "Joined " + thread.getName());
  1736. } catch (InterruptedException ex) {
  1737. Log.w(Helper.TAG, thread.getName() + " join " + ex.toString());
  1738. }
  1739. }
  1740. public static void start(Context context) {
  1741. ContextCompat.startForegroundService(context, new Intent(context, ServiceSynchronize.class));
  1742. }
  1743. public static void reload(Context context, String reason) {
  1744. Log.i(Helper.TAG, "Reload because of '" + reason + "'");
  1745. context.startService(new Intent(context, ServiceSynchronize.class).setAction("reload"));
  1746. }
  1747. private class ServiceState {
  1748. boolean running = true;
  1749. }
  1750. }