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

1595 lines
69 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
  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.app.Notification;
  17. import android.app.NotificationManager;
  18. import android.app.PendingIntent;
  19. import android.content.BroadcastReceiver;
  20. import android.content.ComponentName;
  21. import android.content.Context;
  22. import android.content.Intent;
  23. import android.content.IntentFilter;
  24. import android.content.ServiceConnection;
  25. import android.content.SharedPreferences;
  26. import android.media.RingtoneManager;
  27. import android.net.ConnectivityManager;
  28. import android.net.Network;
  29. import android.net.NetworkCapabilities;
  30. import android.net.NetworkInfo;
  31. import android.net.NetworkRequest;
  32. import android.net.Uri;
  33. import android.os.Binder;
  34. import android.os.Build;
  35. import android.os.Bundle;
  36. import android.os.IBinder;
  37. import android.os.SystemClock;
  38. import android.preference.PreferenceManager;
  39. import android.text.TextUtils;
  40. import android.util.Log;
  41. import com.sun.mail.iap.ProtocolException;
  42. import com.sun.mail.imap.AppendUID;
  43. import com.sun.mail.imap.IMAPFolder;
  44. import com.sun.mail.imap.IMAPMessage;
  45. import com.sun.mail.imap.IMAPStore;
  46. import com.sun.mail.imap.protocol.IMAPProtocol;
  47. import org.json.JSONArray;
  48. import org.json.JSONException;
  49. import java.io.BufferedOutputStream;
  50. import java.io.File;
  51. import java.io.FileOutputStream;
  52. import java.io.IOException;
  53. import java.io.InputStream;
  54. import java.io.OutputStream;
  55. import java.net.SocketTimeoutException;
  56. import java.util.ArrayList;
  57. import java.util.Calendar;
  58. import java.util.Date;
  59. import java.util.HashMap;
  60. import java.util.List;
  61. import java.util.Map;
  62. import java.util.Properties;
  63. import java.util.concurrent.ExecutorService;
  64. import java.util.concurrent.Executors;
  65. import java.util.concurrent.RejectedExecutionException;
  66. import java.util.concurrent.Semaphore;
  67. import javax.mail.Address;
  68. import javax.mail.FetchProfile;
  69. import javax.mail.Flags;
  70. import javax.mail.Folder;
  71. import javax.mail.FolderClosedException;
  72. import javax.mail.FolderNotFoundException;
  73. import javax.mail.Message;
  74. import javax.mail.MessageRemovedException;
  75. import javax.mail.MessagingException;
  76. import javax.mail.NoSuchProviderException;
  77. import javax.mail.SendFailedException;
  78. import javax.mail.Session;
  79. import javax.mail.Transport;
  80. import javax.mail.UIDFolder;
  81. import javax.mail.event.ConnectionAdapter;
  82. import javax.mail.event.ConnectionEvent;
  83. import javax.mail.event.FolderAdapter;
  84. import javax.mail.event.FolderEvent;
  85. import javax.mail.event.MessageChangedEvent;
  86. import javax.mail.event.MessageChangedListener;
  87. import javax.mail.event.MessageCountAdapter;
  88. import javax.mail.event.MessageCountEvent;
  89. import javax.mail.event.StoreEvent;
  90. import javax.mail.event.StoreListener;
  91. import javax.mail.internet.InternetAddress;
  92. import javax.mail.internet.MimeMessage;
  93. import javax.mail.search.ComparisonTerm;
  94. import javax.mail.search.ReceivedDateTerm;
  95. import androidx.annotation.Nullable;
  96. import androidx.core.content.ContextCompat;
  97. import androidx.lifecycle.LifecycleService;
  98. import androidx.lifecycle.Observer;
  99. import androidx.localbroadcastmanager.content.LocalBroadcastManager;
  100. public class ServiceSynchronize extends LifecycleService {
  101. private final Object lock = new Object();
  102. private ServiceManager serviceManager = new ServiceManager();
  103. private static final int NOTIFICATION_SYNCHRONIZE = 1;
  104. private static final int NOTIFICATION_UNSEEN = 2;
  105. private static final int CONNECT_BACKOFF_START = 2; // seconds
  106. private static final int CONNECT_BACKOFF_MAX = 128; // seconds
  107. private static final long STORE_NOOP_INTERVAL = 9 * 60 * 1000L; // ms
  108. private static final long FOLDER_NOOP_INTERVAL = 9 * 60 * 1000L; // ms
  109. private static final int ATTACHMENT_BUFFER_SIZE = 8192; // bytes
  110. static final String ACTION_PROCESS_OPERATIONS = BuildConfig.APPLICATION_ID + ".PROCESS_OPERATIONS";
  111. public ServiceSynchronize() {
  112. // https://docs.oracle.com/javaee/6/api/javax/mail/internet/package-summary.html
  113. System.setProperty("mail.mime.ignoreunknownencoding", "true");
  114. System.setProperty("mail.mime.decodefilename", "true");
  115. System.setProperty("mail.mime.encodefilename", "true");
  116. }
  117. @Override
  118. public void onCreate() {
  119. Log.i(Helper.TAG, "Service create");
  120. super.onCreate();
  121. startForeground(NOTIFICATION_SYNCHRONIZE, getNotificationService(0, 0, 0).build());
  122. // Listen for network changes
  123. ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
  124. NetworkRequest.Builder builder = new NetworkRequest.Builder();
  125. builder.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET);
  126. // Removed because of Android VPN service
  127. // builder.addCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED);
  128. cm.registerNetworkCallback(builder.build(), serviceManager);
  129. DB.getInstance(this).account().liveStats().observe(this, new Observer<TupleAccountStats>() {
  130. private int prev_unseen = -1;
  131. @Override
  132. public void onChanged(@Nullable TupleAccountStats stats) {
  133. NotificationManager nm = getSystemService(NotificationManager.class);
  134. nm.notify(NOTIFICATION_SYNCHRONIZE,
  135. getNotificationService(stats.accounts, stats.operations, stats.unsent).build());
  136. if (stats.unseen > 0) {
  137. if (stats.unseen > prev_unseen) {
  138. nm.cancel(NOTIFICATION_UNSEEN);
  139. nm.notify(NOTIFICATION_UNSEEN, getNotificationUnseen(stats.unseen).build());
  140. }
  141. } else
  142. nm.cancel(NOTIFICATION_UNSEEN);
  143. prev_unseen = stats.unseen;
  144. }
  145. });
  146. }
  147. @Override
  148. public void onDestroy() {
  149. Log.i(Helper.TAG, "Service destroy");
  150. ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
  151. cm.unregisterNetworkCallback(serviceManager);
  152. serviceManager.stop(false);
  153. stopForeground(true);
  154. NotificationManager nm = getSystemService(NotificationManager.class);
  155. nm.cancel(NOTIFICATION_SYNCHRONIZE);
  156. super.onDestroy();
  157. }
  158. @Override
  159. public int onStartCommand(Intent intent, int flags, int startId) {
  160. Log.i(Helper.TAG, "Service start intent=" + intent);
  161. super.onStartCommand(intent, flags, startId);
  162. if (intent != null && "unseen".equals(intent.getAction())) {
  163. Bundle args = new Bundle();
  164. args.putLong("time", new Date().getTime());
  165. new SimpleTask<Void>() {
  166. @Override
  167. protected Void onLoad(Context context, Bundle args) {
  168. long time = args.getLong("time");
  169. DB db = DB.getInstance(context);
  170. try {
  171. db.beginTransaction();
  172. for (EntityAccount account : db.account().getAccounts(true))
  173. db.account().setAccountSeenUntil(account.id, time);
  174. db.setTransactionSuccessful();
  175. } finally {
  176. db.endTransaction();
  177. }
  178. return null;
  179. }
  180. @Override
  181. protected void onLoaded(Bundle args, Void data) {
  182. Log.i(Helper.TAG, "Updated seen until");
  183. }
  184. }.load(ServiceSynchronize.this, args);
  185. }
  186. return START_STICKY;
  187. }
  188. private Notification.Builder getNotificationService(int accounts, int operations, int unsent) {
  189. // Build pending intent
  190. Intent intent = new Intent(this, ActivityView.class);
  191. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  192. PendingIntent pi = PendingIntent.getActivity(
  193. this, ActivityView.REQUEST_VIEW, intent, PendingIntent.FLAG_UPDATE_CURRENT);
  194. // Build notification
  195. Notification.Builder builder;
  196. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
  197. builder = new Notification.Builder(this, "service");
  198. else
  199. builder = new Notification.Builder(this);
  200. builder
  201. .setSmallIcon(R.drawable.baseline_mail_outline_24)
  202. .setContentTitle(getResources().getQuantityString(R.plurals.title_notification_synchronizing, accounts, accounts))
  203. .setContentIntent(pi)
  204. .setAutoCancel(false)
  205. .setShowWhen(false)
  206. .setPriority(Notification.PRIORITY_MIN)
  207. .setCategory(Notification.CATEGORY_STATUS)
  208. .setVisibility(Notification.VISIBILITY_SECRET);
  209. if (operations > 0)
  210. builder.setStyle(new Notification.BigTextStyle().setSummaryText(
  211. getResources().getQuantityString(R.plurals.title_notification_operations, operations, operations)));
  212. if (unsent > 0)
  213. builder.setContentText(getResources().getQuantityString(R.plurals.title_notification_unsent, unsent, unsent));
  214. return builder;
  215. }
  216. private Notification.Builder getNotificationUnseen(int unseen) {
  217. // Build pending intent
  218. Intent intent = new Intent(this, ActivityView.class);
  219. intent.setAction("unseen");
  220. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  221. PendingIntent pi = PendingIntent.getActivity(
  222. this, ActivityView.REQUEST_UNSEEN, intent, PendingIntent.FLAG_UPDATE_CURRENT);
  223. Intent delete = new Intent(this, ServiceSynchronize.class);
  224. delete.setAction("unseen");
  225. PendingIntent pid = PendingIntent.getService(this, 1, delete, PendingIntent.FLAG_UPDATE_CURRENT);
  226. Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
  227. // Build notification
  228. Notification.Builder builder;
  229. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
  230. builder = new Notification.Builder(this, "notification");
  231. else
  232. builder = new Notification.Builder(this);
  233. builder
  234. .setSmallIcon(R.drawable.baseline_mail_24)
  235. .setContentTitle(getResources().getQuantityString(R.plurals.title_notification_unseen, unseen, unseen))
  236. .setContentIntent(pi)
  237. .setSound(uri)
  238. .setShowWhen(false)
  239. .setPriority(Notification.PRIORITY_DEFAULT)
  240. .setCategory(Notification.CATEGORY_STATUS)
  241. .setVisibility(Notification.VISIBILITY_PUBLIC)
  242. .setDeleteIntent(pid);
  243. return builder;
  244. }
  245. private Notification.Builder getNotificationError(String action, Throwable ex) {
  246. // Build pending intent
  247. Intent intent = new Intent(this, ActivityView.class);
  248. intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  249. PendingIntent pi = PendingIntent.getActivity(
  250. this, ActivityView.REQUEST_VIEW, intent, PendingIntent.FLAG_UPDATE_CURRENT);
  251. // Build notification
  252. Notification.Builder builder;
  253. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
  254. builder = new Notification.Builder(this, "error");
  255. else
  256. builder = new Notification.Builder(this);
  257. builder
  258. .setSmallIcon(android.R.drawable.stat_notify_error)
  259. .setContentTitle(getString(R.string.title_notification_failed, action))
  260. .setContentText(Helper.formatThrowable(ex))
  261. .setContentIntent(pi)
  262. .setAutoCancel(false)
  263. .setShowWhen(true)
  264. .setPriority(Notification.PRIORITY_MAX)
  265. .setCategory(Notification.CATEGORY_ERROR)
  266. .setVisibility(Notification.VISIBILITY_SECRET);
  267. return builder;
  268. }
  269. private void reportError(String account, String folder, Throwable ex) {
  270. // FolderClosedException: can happen when no connectivity
  271. // IllegalStateException:
  272. // - "This operation is not allowed on a closed folder"
  273. // - can happen when syncing message
  274. if (!(ex instanceof FolderClosedException) && !(ex instanceof IllegalStateException)) {
  275. String action = account + "/" + folder;
  276. NotificationManager nm = getSystemService(NotificationManager.class);
  277. nm.notify(action, 1, getNotificationError(action, ex).build());
  278. }
  279. }
  280. private void monitorAccount(final EntityAccount account, final ServiceState state) throws NoSuchProviderException {
  281. Log.i(Helper.TAG, account.name + " start");
  282. final DB db = DB.getInstance(this);
  283. final ExecutorService executor = Executors.newSingleThreadExecutor();
  284. Properties props = MessageHelper.getSessionProperties();
  285. props.setProperty("mail.imaps.peek", "true");
  286. props.setProperty("mail.mime.address.strict", "false");
  287. props.setProperty("mail.mime.decodetext.strict", "false");
  288. //props.put("mail.imaps.minidletime", "5000");
  289. boolean debug = PreferenceManager.getDefaultSharedPreferences(this).getBoolean("debug", false);
  290. if (debug)
  291. System.setProperty("mail.socket.debug", "true");
  292. final Session isession = Session.getInstance(props, null);
  293. isession.setDebug(debug);
  294. // adb -t 1 logcat | grep "fairemail\|System.out"
  295. int backoff = CONNECT_BACKOFF_START;
  296. while (state.running) {
  297. final IMAPStore istore = (IMAPStore) isession.getStore("imaps");
  298. final Map<EntityFolder, IMAPFolder> folders = new HashMap<>();
  299. List<Thread> noops = new ArrayList<>();
  300. List<Thread> idlers = new ArrayList<>();
  301. try {
  302. // Listen for store events
  303. istore.addStoreListener(new StoreListener() {
  304. @Override
  305. public void notification(StoreEvent e) {
  306. Log.i(Helper.TAG, account.name + " event: " + e.getMessage());
  307. db.account().setAccountError(account.id, e.getMessage());
  308. synchronized (state) {
  309. state.notifyAll();
  310. }
  311. }
  312. });
  313. // Listen for folder events
  314. istore.addFolderListener(new FolderAdapter() {
  315. @Override
  316. public void folderCreated(FolderEvent e) {
  317. // TODO: folder created
  318. }
  319. @Override
  320. public void folderRenamed(FolderEvent e) {
  321. // TODO: folder renamed
  322. }
  323. @Override
  324. public void folderDeleted(FolderEvent e) {
  325. // TODO: folder deleted
  326. }
  327. });
  328. // Listen for connection events
  329. istore.addConnectionListener(new ConnectionAdapter() {
  330. @Override
  331. public void opened(ConnectionEvent e) {
  332. Log.i(Helper.TAG, account.name + " opened");
  333. }
  334. @Override
  335. public void disconnected(ConnectionEvent e) {
  336. Log.e(Helper.TAG, account.name + " disconnected event");
  337. }
  338. @Override
  339. public void closed(ConnectionEvent e) {
  340. Log.e(Helper.TAG, account.name + " closed event");
  341. }
  342. });
  343. // Initiate connection
  344. Log.i(Helper.TAG, account.name + " connect");
  345. db.account().setAccountState(account.id, "connecting");
  346. istore.connect(account.host, account.port, account.user, account.password);
  347. backoff = CONNECT_BACKOFF_START;
  348. db.account().setAccountState(account.id, "connected");
  349. db.account().setAccountError(account.id, null);
  350. // Update folder list
  351. try {
  352. synchronizeFolders(account, istore);
  353. } catch (MessagingException ex) {
  354. // Don't show to user
  355. throw new IllegalStateException("synchronize folders", ex);
  356. }
  357. // Synchronize folders
  358. for (EntityFolder folder : db.folder().getFolders(account.id))
  359. db.folder().setFolderState(folder.id, null);
  360. for (final EntityFolder folder : db.folder().getFolders(account.id, true))
  361. try {
  362. Log.i(Helper.TAG, account.name + " sync folder " + folder.name);
  363. db.folder().setFolderState(folder.id, "connecting");
  364. final IMAPFolder ifolder = (IMAPFolder) istore.getFolder(folder.name);
  365. ifolder.open(Folder.READ_WRITE);
  366. folders.put(folder, ifolder);
  367. db.folder().setFolderState(folder.id, "connected");
  368. db.folder().setFolderError(folder.id, null);
  369. // Listen for new and deleted messages
  370. ifolder.addMessageCountListener(new MessageCountAdapter() {
  371. @Override
  372. public void messagesAdded(MessageCountEvent e) {
  373. synchronized (lock) {
  374. try {
  375. Log.i(Helper.TAG, folder.name + " messages added");
  376. for (Message imessage : e.getMessages())
  377. try {
  378. synchronizeMessage(folder, ifolder, (IMAPMessage) imessage);
  379. } catch (MessageRemovedException ex) {
  380. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  381. }
  382. } catch (Throwable ex) {
  383. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  384. reportError(account.name, folder.name, ex);
  385. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  386. synchronized (state) {
  387. state.notifyAll();
  388. }
  389. }
  390. }
  391. }
  392. @Override
  393. public void messagesRemoved(MessageCountEvent e) {
  394. synchronized (lock) {
  395. try {
  396. Log.i(Helper.TAG, folder.name + " messages removed");
  397. for (Message imessage : e.getMessages())
  398. try {
  399. long uid = ifolder.getUID(imessage);
  400. DB db = DB.getInstance(ServiceSynchronize.this);
  401. int count = db.message().deleteMessage(folder.id, uid);
  402. Log.i(Helper.TAG, "Deleted uid=" + uid + " count=" + count);
  403. } catch (MessageRemovedException ex) {
  404. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  405. }
  406. } catch (Throwable ex) {
  407. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  408. reportError(account.name, folder.name, ex);
  409. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  410. synchronized (state) {
  411. state.notifyAll();
  412. }
  413. }
  414. }
  415. }
  416. });
  417. // Fetch e-mail
  418. synchronizeMessages(account, folder, ifolder);
  419. // Flags (like "seen") at the remote could be changed while synchronizing
  420. // Listen for changed messages
  421. ifolder.addMessageChangedListener(new MessageChangedListener() {
  422. @Override
  423. public void messageChanged(MessageChangedEvent e) {
  424. synchronized (lock) {
  425. try {
  426. try {
  427. Log.i(Helper.TAG, folder.name + " message changed");
  428. synchronizeMessage(folder, ifolder, (IMAPMessage) e.getMessage());
  429. } catch (MessageRemovedException ex) {
  430. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  431. }
  432. } catch (Throwable ex) {
  433. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  434. reportError(account.name, folder.name, ex);
  435. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  436. synchronized (state) {
  437. state.notifyAll();
  438. }
  439. }
  440. }
  441. }
  442. });
  443. // Keep folder connection alive
  444. Thread noop = new Thread(new Runnable() {
  445. @Override
  446. public void run() {
  447. try {
  448. Log.i(Helper.TAG, folder.name + " start noop");
  449. while (state.running && ifolder.isOpen()) {
  450. Log.i(Helper.TAG, folder.name + " request NOOP");
  451. ifolder.doCommand(new IMAPFolder.ProtocolCommand() {
  452. public Object doCommand(IMAPProtocol p) throws ProtocolException {
  453. Log.i(Helper.TAG, ifolder.getName() + " start NOOP");
  454. p.simpleCommand("NOOP", null);
  455. Log.i(Helper.TAG, ifolder.getName() + " end NOOP");
  456. return null;
  457. }
  458. });
  459. try {
  460. Thread.sleep(FOLDER_NOOP_INTERVAL);
  461. } catch (InterruptedException ex) {
  462. Log.w(Helper.TAG, folder.name + " noop " + ex.getMessage());
  463. }
  464. }
  465. } catch (Throwable ex) {
  466. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  467. reportError(account.name, folder.name, ex);
  468. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  469. synchronized (state) {
  470. state.notifyAll();
  471. }
  472. } finally {
  473. Log.i(Helper.TAG, folder.name + " end noop");
  474. }
  475. }
  476. }, "sync.noop." + folder.id);
  477. noop.start();
  478. noops.add(noop);
  479. // Receive folder events
  480. Thread idle = new Thread(new Runnable() {
  481. @Override
  482. public void run() {
  483. try {
  484. Log.i(Helper.TAG, folder.name + " start idle");
  485. while (state.running && ifolder.isOpen()) {
  486. Log.i(Helper.TAG, folder.name + " do idle");
  487. ifolder.idle(false);
  488. Log.i(Helper.TAG, folder.name + " done idle");
  489. }
  490. } catch (Throwable ex) {
  491. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  492. reportError(account.name, folder.name, ex);
  493. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  494. synchronized (state) {
  495. state.notifyAll();
  496. }
  497. } finally {
  498. Log.i(Helper.TAG, folder.name + " end idle");
  499. }
  500. }
  501. }, "sync.idle." + folder.id);
  502. idle.start();
  503. idlers.add(idle);
  504. } catch (MessagingException ex) {
  505. // Don't show to user
  506. throw new FolderClosedException(folders.get(folder), "start folder", ex);
  507. } catch (IOException ex) {
  508. // Don't show to user
  509. throw new FolderClosedException(folders.get(folder), "start folder", ex);
  510. }
  511. BroadcastReceiver processReceiver = new BroadcastReceiver() {
  512. @Override
  513. public void onReceive(Context context, Intent intent) {
  514. final long fid = intent.getLongExtra("folder", -1);
  515. //Log.v(Helper.TAG, "run operations folder=" + fid);
  516. try {
  517. executor.submit(new Runnable() {
  518. @Override
  519. public void run() {
  520. // Get folder
  521. EntityFolder folder = null;
  522. IMAPFolder ifolder = null;
  523. for (EntityFolder f : folders.keySet())
  524. if (f.id == fid) {
  525. folder = f;
  526. ifolder = folders.get(f);
  527. break;
  528. }
  529. final boolean shouldClose = (ifolder == null);
  530. try {
  531. if (folder == null)
  532. throw new IllegalArgumentException("Unknown folder=" + fid);
  533. if (shouldClose)
  534. Log.v(Helper.TAG, folder.name + " start operations offline=" + shouldClose);
  535. if (ifolder == null) {
  536. // Prevent unnecessary folder connections
  537. if (db.operation().getOperationCount(fid) == 0)
  538. return;
  539. ifolder = (IMAPFolder) istore.getFolder(folder.name);
  540. ifolder.open(Folder.READ_WRITE);
  541. }
  542. processOperations(folder, isession, istore, ifolder);
  543. } catch (Throwable ex) {
  544. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  545. reportError(account.name, folder.name, ex);
  546. } finally {
  547. if (shouldClose)
  548. if (ifolder != null && ifolder.isOpen()) {
  549. try {
  550. ifolder.close(false);
  551. } catch (MessagingException ex) {
  552. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  553. }
  554. }
  555. //Log.v(Helper.TAG, folder.name + " stop operations");
  556. }
  557. }
  558. });
  559. } catch (RejectedExecutionException ex) {
  560. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  561. }
  562. }
  563. };
  564. // Listen for folder operations
  565. IntentFilter f = new IntentFilter(ACTION_PROCESS_OPERATIONS);
  566. f.addDataType("account/" + account.id);
  567. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
  568. lbm.registerReceiver(processReceiver, f);
  569. try {
  570. // Process pending folder operations
  571. Log.i(Helper.TAG, "listen process folder");
  572. for (final EntityFolder folder : folders.keySet())
  573. if (!EntityFolder.OUTBOX.equals(folder.type))
  574. lbm.sendBroadcast(new Intent(ACTION_PROCESS_OPERATIONS)
  575. .setType("account/" + account.id)
  576. .putExtra("folder", folder.id));
  577. // Keep store alive
  578. while (state.running && istore.isConnected()) {
  579. Log.i(Helper.TAG, "Checking folders");
  580. for (EntityFolder folder : folders.keySet())
  581. if (!folders.get(folder).isOpen())
  582. throw new FolderClosedException(folders.get(folder));
  583. // Wait for stop or folder error
  584. Log.i(Helper.TAG, account.name + " wait");
  585. synchronized (state) {
  586. state.wait(STORE_NOOP_INTERVAL);
  587. }
  588. Log.i(Helper.TAG, account.name + " waited");
  589. }
  590. Log.i(Helper.TAG, account.name + " done running=" + state.running);
  591. } finally {
  592. lbm.unregisterReceiver(processReceiver);
  593. }
  594. } catch (Throwable ex) {
  595. Log.e(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  596. reportError(account.name, null, ex);
  597. db.account().setAccountError(account.id, Helper.formatThrowable(ex));
  598. } finally {
  599. // Close store
  600. Log.i(Helper.TAG, account.name + " closing");
  601. db.account().setAccountState(account.id, "closing");
  602. try {
  603. // This can take some time
  604. istore.close();
  605. } catch (MessagingException ex) {
  606. Log.w(Helper.TAG, account.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  607. } finally {
  608. Log.i(Helper.TAG, account.name + " closed");
  609. db.account().setAccountState(account.id, null);
  610. for (EntityFolder folder : folders.keySet())
  611. db.folder().setFolderState(folder.id, null);
  612. }
  613. // Stop noop
  614. for (Thread noop : noops) {
  615. noop.interrupt();
  616. join(noop);
  617. }
  618. // Stop idle
  619. for (Thread idle : idlers) {
  620. idle.interrupt();
  621. join(idle);
  622. }
  623. }
  624. if (state.running) {
  625. try {
  626. Log.i(Helper.TAG, "Backoff seconds=" + backoff);
  627. Thread.sleep(backoff * 1000L);
  628. if (backoff < CONNECT_BACKOFF_MAX)
  629. backoff *= 2;
  630. } catch (InterruptedException ex) {
  631. Log.w(Helper.TAG, account.name + " backoff " + ex.getMessage());
  632. }
  633. }
  634. }
  635. Log.i(Helper.TAG, account.name + " stopped");
  636. }
  637. private void processOperations(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder) throws MessagingException, JSONException, IOException {
  638. synchronized (lock) {
  639. try {
  640. Log.i(Helper.TAG, folder.name + " start process");
  641. DB db = DB.getInstance(this);
  642. List<EntityOperation> ops = db.operation().getOperationsByFolder(folder.id);
  643. Log.i(Helper.TAG, folder.name + " pending operations=" + ops.size());
  644. for (EntityOperation op : ops)
  645. try {
  646. Log.i(Helper.TAG, folder.name +
  647. " start op=" + op.id + "/" + op.name +
  648. " msg=" + op.message +
  649. " args=" + op.args);
  650. EntityMessage message = db.message().getMessage(op.message);
  651. if (message == null)
  652. throw new MessageRemovedException();
  653. if (message.uid == null &&
  654. (EntityOperation.SEEN.equals(op.name) ||
  655. EntityOperation.DELETE.equals(op.name) ||
  656. EntityOperation.MOVE.equals(op.name)))
  657. throw new IllegalArgumentException(op.name + " without uid");
  658. try {
  659. db.message().setMessageError(message.id, null);
  660. JSONArray jargs = new JSONArray(op.args);
  661. if (EntityOperation.SEEN.equals(op.name))
  662. doSeen(folder, ifolder, message, jargs, db);
  663. else if (EntityOperation.ADD.equals(op.name))
  664. doAdd(folder, isession, ifolder, message, jargs, db);
  665. else if (EntityOperation.MOVE.equals(op.name))
  666. doMove(folder, isession, istore, ifolder, message, jargs, db);
  667. else if (EntityOperation.DELETE.equals(op.name))
  668. doDelete(folder, ifolder, message, jargs, db);
  669. else if (EntityOperation.SEND.equals(op.name))
  670. doSend(isession, message, db);
  671. else if (EntityOperation.ATTACHMENT.equals(op.name))
  672. doAttachment(folder, op, ifolder, message, jargs, db);
  673. else
  674. throw new MessagingException("Unknown operation name=" + op.name);
  675. // Operation succeeded
  676. db.operation().deleteOperation(op.id);
  677. } catch (Throwable ex) {
  678. // TODO: SMTP response codes: https://www.ietf.org/rfc/rfc821.txt
  679. if (ex instanceof SendFailedException)
  680. reportError(null, folder.name, ex);
  681. db.message().setMessageError(message.id, Helper.formatThrowable(ex));
  682. if (ex instanceof MessageRemovedException ||
  683. ex instanceof FolderNotFoundException ||
  684. ex instanceof SendFailedException) {
  685. Log.w(Helper.TAG, "Unrecoverable " + ex + "\n" + Log.getStackTraceString(ex));
  686. // There is no use in repeating
  687. db.operation().deleteOperation(op.id);
  688. continue;
  689. } else if (ex instanceof MessagingException) {
  690. // Socket timeout is a recoverable condition (send message)
  691. if (ex.getCause() instanceof SocketTimeoutException) {
  692. Log.w(Helper.TAG, "Recoverable " + ex + "\n" + Log.getStackTraceString(ex));
  693. // No need to inform user
  694. return;
  695. }
  696. }
  697. throw ex;
  698. }
  699. } finally {
  700. Log.i(Helper.TAG, folder.name + " end op=" + op.id + "/" + op.name);
  701. }
  702. } finally {
  703. Log.i(Helper.TAG, folder.name + " end process");
  704. }
  705. }
  706. }
  707. private void doSeen(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
  708. // Mark message (un)seen
  709. boolean seen = jargs.getBoolean(0);
  710. Message imessage = ifolder.getMessageByUID(message.uid);
  711. if (imessage == null)
  712. throw new MessageRemovedException();
  713. imessage.setFlag(Flags.Flag.SEEN, seen);
  714. db.message().setMessageSeen(message.id, seen);
  715. }
  716. private void doAdd(EntityFolder folder, Session isession, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
  717. // Append message
  718. List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
  719. MimeMessage imessage = MessageHelper.from(this, message, attachments, isession);
  720. AppendUID[] uid = ifolder.appendUIDMessages(new Message[]{imessage});
  721. if (message.uid != null) {
  722. Message iprev = ifolder.getMessageByUID(message.uid);
  723. if (iprev != null) {
  724. Log.i(Helper.TAG, "Deleting existing id=" + message.id);
  725. iprev.setFlag(Flags.Flag.DELETED, true);
  726. ifolder.expunge();
  727. }
  728. }
  729. db.message().setMessageUid(message.id, uid[0].uid);
  730. Log.i(Helper.TAG, "Appended uid=" + message.uid);
  731. }
  732. private void doMove(EntityFolder folder, Session isession, IMAPStore istore, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws JSONException, MessagingException {
  733. // Move message
  734. long id = jargs.getLong(0);
  735. EntityFolder target = db.folder().getFolder(id);
  736. if (target == null)
  737. throw new FolderNotFoundException();
  738. // Get message
  739. Message imessage = ifolder.getMessageByUID(message.uid);
  740. if (imessage == null)
  741. throw new MessageRemovedException();
  742. if (istore.hasCapability("MOVE")) {
  743. Folder itarget = istore.getFolder(target.name);
  744. ifolder.moveMessages(new Message[]{imessage}, itarget);
  745. } else {
  746. Log.w(Helper.TAG, "MOVE by DELETE/APPEND");
  747. List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
  748. if (!EntityFolder.ARCHIVE.equals(folder.type)) {
  749. imessage.setFlag(Flags.Flag.DELETED, true);
  750. ifolder.expunge();
  751. }
  752. MimeMessageEx icopy = MessageHelper.from(this, message, attachments, isession);
  753. Folder itarget = istore.getFolder(target.name);
  754. itarget.appendMessages(new Message[]{icopy});
  755. }
  756. }
  757. private void doDelete(EntityFolder folder, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws MessagingException, JSONException {
  758. // Delete message
  759. Message imessage = ifolder.getMessageByUID(message.uid);
  760. if (imessage == null)
  761. throw new MessageRemovedException();
  762. imessage.setFlag(Flags.Flag.DELETED, true);
  763. ifolder.expunge();
  764. db.message().deleteMessage(message.id);
  765. }
  766. private void doSend(Session isession, EntityMessage message, DB db) throws MessagingException {
  767. // Send message
  768. EntityIdentity ident = db.identity().getIdentity(message.identity);
  769. EntityMessage reply = (message.replying == null ? null : db.message().getMessage(message.replying));
  770. List<EntityAttachment> attachments = db.attachment().getAttachments(message.id);
  771. if (!ident.synchronize) {
  772. // Message will remain in outbox
  773. return;
  774. }
  775. // Create message
  776. MimeMessage imessage;
  777. if (reply == null)
  778. imessage = MessageHelper.from(this, message, attachments, isession);
  779. else
  780. imessage = MessageHelper.from(this, message, reply, attachments, isession);
  781. if (ident.replyto != null)
  782. imessage.setReplyTo(new Address[]{new InternetAddress(ident.replyto)});
  783. // Create transport
  784. // TODO: cache transport?
  785. Transport itransport = isession.getTransport(ident.starttls ? "smtp" : "smtps");
  786. try {
  787. // Connect transport
  788. db.identity().setIdentityState(ident.id, "connecting");
  789. itransport.connect(ident.host, ident.port, ident.user, ident.password);
  790. db.identity().setIdentityState(ident.id, "connected");
  791. db.identity().setIdentityError(ident.id, null);
  792. // Send message
  793. Address[] to = imessage.getAllRecipients();
  794. itransport.sendMessage(imessage, to);
  795. Log.i(Helper.TAG, "Sent via " + ident.host + "/" + ident.user +
  796. " to " + TextUtils.join(", ", to));
  797. try {
  798. db.beginTransaction();
  799. // Mark message as sent
  800. // - will be moved to sent folder by synchronize message later
  801. message.sent = imessage.getSentDate().getTime();
  802. message.seen = true;
  803. message.ui_seen = true;
  804. db.message().updateMessage(message);
  805. // TODO: store sent setting per account
  806. SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
  807. if (prefs.getBoolean("store_sent", false)) {
  808. EntityFolder sent = db.folder().getFolderByType(ident.account, EntityFolder.SENT);
  809. if (sent != null) {
  810. // TODO: how to handle thread?
  811. Log.i(Helper.TAG, "Appending sent msgid=" + message.msgid);
  812. EntityOperation.queue(db, message, EntityOperation.ADD); // Could already exist
  813. }
  814. }
  815. db.setTransactionSuccessful();
  816. } finally {
  817. db.endTransaction();
  818. }
  819. EntityOperation.process(this);
  820. } catch (MessagingException ex) {
  821. db.identity().setIdentityError(ident.id, Helper.formatThrowable(ex));
  822. throw ex;
  823. } finally {
  824. try {
  825. itransport.close();
  826. } finally {
  827. db.identity().setIdentityState(ident.id, null);
  828. }
  829. }
  830. }
  831. private void doAttachment(EntityFolder folder, EntityOperation op, IMAPFolder ifolder, EntityMessage message, JSONArray jargs, DB db) throws JSONException, MessagingException, IOException {
  832. // Download attachment
  833. int sequence = jargs.getInt(0);
  834. EntityAttachment attachment = db.attachment().getAttachment(op.message, sequence);
  835. if (attachment == null)
  836. return;
  837. try {
  838. // Get message
  839. Message imessage = ifolder.getMessageByUID(message.uid);
  840. if (imessage == null)
  841. throw new MessageRemovedException();
  842. // Get attachment
  843. MessageHelper helper = new MessageHelper((MimeMessage) imessage);
  844. EntityAttachment a = helper.getAttachments().get(sequence - 1);
  845. // Build filename
  846. File dir = new File(getFilesDir(), "attachments");
  847. dir.mkdir();
  848. File file = new File(dir, Long.toString(attachment.id));
  849. // Download attachment
  850. InputStream is = null;
  851. OutputStream os = null;
  852. try {
  853. is = a.part.getInputStream();
  854. os = new BufferedOutputStream(new FileOutputStream(file));
  855. int size = 0;
  856. byte[] buffer = new byte[ATTACHMENT_BUFFER_SIZE];
  857. for (int len = is.read(buffer); len != -1; len = is.read(buffer)) {
  858. size += len;
  859. os.write(buffer, 0, len);
  860. // Update progress
  861. if (attachment.size != null)
  862. db.attachment().setProgress(attachment.id, size * 100 / attachment.size);
  863. }
  864. // Store attachment data
  865. attachment.size = size;
  866. attachment.progress = null;
  867. attachment.filename = file.getName();
  868. db.attachment().updateAttachment(attachment);
  869. } finally {
  870. try {
  871. if (is != null)
  872. is.close();
  873. } finally {
  874. if (os != null)
  875. os.close();
  876. }
  877. }
  878. Log.i(Helper.TAG, folder.name + " downloaded bytes=" + attachment.size);
  879. } catch (Throwable ex) {
  880. // Reset progress on failure
  881. attachment.progress = null;
  882. attachment.filename = null;
  883. db.attachment().updateAttachment(attachment);
  884. throw ex;
  885. }
  886. }
  887. private void synchronizeFolders(EntityAccount account, IMAPStore istore) throws MessagingException {
  888. try {
  889. Log.v(Helper.TAG, "Start sync folders");
  890. DB db = DB.getInstance(this);
  891. List<String> names = new ArrayList<>();
  892. for (EntityFolder folder : db.folder().getUserFolders(account.id))
  893. names.add(folder.name);
  894. Log.i(Helper.TAG, "Local folder count=" + names.size());
  895. Folder[] ifolders = istore.getDefaultFolder().list("*"); // TODO: is the pattern correct?
  896. Log.i(Helper.TAG, "Remote folder count=" + ifolders.length);
  897. for (Folder ifolder : ifolders) {
  898. String[] attrs = ((IMAPFolder) ifolder).getAttributes();
  899. boolean selectable = true;
  900. for (String attr : attrs) {
  901. if ("\\Noselect".equals(attr)) { // TODO: is this attribute correct?
  902. selectable = false;
  903. break;
  904. }
  905. if (attr.startsWith("\\"))
  906. if (EntityFolder.SYSTEM_FOLDER_ATTR.contains(attr.substring(1))) {
  907. selectable = false;
  908. break;
  909. }
  910. }
  911. if (selectable) {
  912. Log.i(Helper.TAG, ifolder.getFullName() + " candidate attr=" + TextUtils.join(",", attrs));
  913. EntityFolder folder = db.folder().getFolderByName(account.id, ifolder.getFullName());
  914. if (folder == null) {
  915. folder = new EntityFolder();
  916. folder.account = account.id;
  917. folder.name = ifolder.getFullName();
  918. folder.type = EntityFolder.USER;
  919. folder.synchronize = false;
  920. folder.after = EntityFolder.DEFAULT_USER_SYNC;
  921. db.folder().insertFolder(folder);
  922. Log.i(Helper.TAG, folder.name + " added");
  923. } else
  924. names.remove(folder.name);
  925. }
  926. }
  927. Log.i(Helper.TAG, "Delete local folder=" + names.size());
  928. for (String name : names)
  929. db.folder().deleteFolder(account.id, name);
  930. } finally {
  931. Log.v(Helper.TAG, "End sync folder");
  932. }
  933. }
  934. private void synchronizeMessages(EntityAccount account, EntityFolder folder, IMAPFolder ifolder) throws MessagingException, IOException {
  935. try {
  936. Log.v(Helper.TAG, folder.name + " start sync after=" + folder.after);
  937. DB db = DB.getInstance(this);
  938. // Get reference times
  939. Calendar cal = Calendar.getInstance();
  940. cal.add(Calendar.DAY_OF_MONTH, -folder.after);
  941. cal.set(Calendar.HOUR_OF_DAY, 0);
  942. cal.set(Calendar.MINUTE, 0);
  943. cal.set(Calendar.SECOND, 0);
  944. cal.set(Calendar.MILLISECOND, 0);
  945. long ago = cal.getTimeInMillis();
  946. Log.i(Helper.TAG, folder.name + " ago=" + new Date(ago));
  947. // Delete old local messages
  948. int old = db.message().deleteMessagesBefore(folder.id, ago);
  949. Log.i(Helper.TAG, folder.name + " local old=" + old);
  950. // Get list of local uids
  951. List<Long> uids = db.message().getUids(folder.id, ago);
  952. Log.i(Helper.TAG, folder.name + " local count=" + uids.size());
  953. // Reduce list of local uids
  954. long search = SystemClock.elapsedRealtime();
  955. Message[] imessages = ifolder.search(new ReceivedDateTerm(ComparisonTerm.GE, new Date(ago)));
  956. Log.i(Helper.TAG, folder.name + " remote count=" + imessages.length +
  957. " search=" + (SystemClock.elapsedRealtime() - search) + " ms");
  958. FetchProfile fp = new FetchProfile();
  959. fp.add(UIDFolder.FetchProfileItem.UID);
  960. fp.add(IMAPFolder.FetchProfileItem.FLAGS);
  961. ifolder.fetch(imessages, fp);
  962. long fetch = SystemClock.elapsedRealtime();
  963. Log.i(Helper.TAG, folder.name + " remote fetched=" + (SystemClock.elapsedRealtime() - fetch) + " ms");
  964. for (Message imessage : imessages)
  965. try {
  966. uids.remove(ifolder.getUID(imessage));
  967. } catch (MessageRemovedException ex) {
  968. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  969. } catch (Throwable ex) {
  970. Log.e(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  971. reportError(account.name, folder.name, ex);
  972. db.folder().setFolderError(folder.id, Helper.formatThrowable(ex));
  973. }
  974. // Delete local messages not at remote
  975. Log.i(Helper.TAG, folder.name + " delete=" + uids.size());
  976. for (Long uid : uids) {
  977. int count = db.message().deleteMessage(folder.id, uid);
  978. Log.i(Helper.TAG, folder.name + " delete local uid=" + uid + " count=" + count);
  979. }
  980. // Add/update local messages
  981. int added = 0;
  982. int updated = 0;
  983. int unchanged = 0;
  984. Log.i(Helper.TAG, folder.name + " add=" + imessages.length);
  985. for (Message imessage : imessages)
  986. try {
  987. int status = synchronizeMessage(folder, ifolder, (IMAPMessage) imessage);
  988. if (status > 0)
  989. added++;
  990. else if (status < 0)
  991. updated++;
  992. else
  993. unchanged++;
  994. } catch (MessageRemovedException ex) {
  995. Log.w(Helper.TAG, folder.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  996. }
  997. Log.w(Helper.TAG, folder.name + " statistics added=" + added + " updated=" + updated + " unchanged=" + unchanged);
  998. } finally {
  999. Log.v(Helper.TAG, folder.name + " end sync");
  1000. }
  1001. }
  1002. private int synchronizeMessage(EntityFolder folder, IMAPFolder ifolder, IMAPMessage imessage) throws MessagingException, IOException {
  1003. long uid;
  1004. try {
  1005. FetchProfile fp = new FetchProfile();
  1006. fp.add(UIDFolder.FetchProfileItem.UID);
  1007. fp.add(IMAPFolder.FetchProfileItem.FLAGS);
  1008. ifolder.fetch(new Message[]{imessage}, fp);
  1009. uid = ifolder.getUID(imessage);
  1010. //Log.v(Helper.TAG, folder.name + " start sync uid=" + uid);
  1011. if (imessage.isExpunged()) {
  1012. Log.i(Helper.TAG, folder.name + " expunged uid=" + uid);
  1013. return 0;
  1014. }
  1015. if (imessage.isSet(Flags.Flag.DELETED)) {
  1016. Log.i(Helper.TAG, folder.name + " deleted uid=" + uid);
  1017. return 0;
  1018. }
  1019. MessageHelper helper = new MessageHelper(imessage);
  1020. boolean seen = helper.getSeen();
  1021. DB db = DB.getInstance(this);
  1022. try {
  1023. int result = 0;
  1024. db.beginTransaction();
  1025. // Find message by uid (fast, no headers required)
  1026. EntityMessage message = db.message().getMessageByUid(folder.id, uid);
  1027. // Find message by Message-ID (slow, headers required)
  1028. // - messages in inbox have same id as message sent to self
  1029. // - messages in archive have same id as original
  1030. if (message == null) {
  1031. // Will fetch headers within database transaction
  1032. String msgid = helper.getMessageID();
  1033. for (EntityMessage dup : db.message().getMessageByMsgId(folder.account, msgid)) {
  1034. EntityFolder dfolder = db.folder().getFolder(dup.folder);
  1035. boolean outbox = EntityFolder.OUTBOX.equals(dfolder.type);
  1036. Log.i(Helper.TAG, folder.name + " found as id=" + dup.id +
  1037. " folder=" + dfolder.type + ":" + dup.folder + "/" + folder.type + ":" + folder.id);
  1038. if (dup.folder.equals(folder.id) || outbox) {
  1039. Log.i(Helper.TAG, folder.name + " found as id=" + dup.id + " uid=" + dup.uid + " msgid=" + msgid);
  1040. dup.folder = folder.id;
  1041. dup.uid = uid;
  1042. if (outbox) // only now the uid is known
  1043. dup.thread = helper.getThreadId(uid);
  1044. db.message().updateMessage(dup);
  1045. message = dup;
  1046. result = -1;
  1047. }
  1048. }
  1049. }
  1050. if (message != null) {
  1051. if (message.seen != seen || message.seen != message.ui_seen) {
  1052. message.seen = seen;
  1053. message.ui_seen = seen;
  1054. db.message().updateMessage(message);
  1055. Log.i(Helper.TAG, folder.name + " updated id=" + message.id + " uid=" + message.uid + " seen=" + seen);
  1056. result = -1;
  1057. } else
  1058. ; //Log.v(Helper.TAG, folder.name + " unchanged id=" + message.id + " uid=" + message.uid);
  1059. }
  1060. db.setTransactionSuccessful();
  1061. if (message != null)
  1062. return result;
  1063. } finally {
  1064. db.endTransaction();
  1065. }
  1066. FetchProfile fp1 = new FetchProfile();
  1067. fp1.add(FetchProfile.Item.ENVELOPE);
  1068. fp1.add(FetchProfile.Item.CONTENT_INFO);
  1069. fp1.add(IMAPFolder.FetchProfileItem.HEADERS);
  1070. fp1.add(IMAPFolder.FetchProfileItem.MESSAGE);
  1071. ifolder.fetch(new Message[]{imessage}, fp1);
  1072. try {
  1073. db.beginTransaction();
  1074. EntityMessage message = new EntityMessage();
  1075. message.account = folder.account;
  1076. message.folder = folder.id;
  1077. message.uid = uid;
  1078. if (!EntityFolder.ARCHIVE.equals(folder.type)) {
  1079. message.msgid = helper.getMessageID();
  1080. if (TextUtils.isEmpty(message.msgid))
  1081. Log.w(Helper.TAG, "No Message-ID id=" + message.id + " uid=" + message.uid);
  1082. }
  1083. message.references = TextUtils.join(" ", helper.getReferences());
  1084. message.inreplyto = helper.getInReplyTo();
  1085. message.thread = helper.getThreadId(uid);
  1086. message.from = helper.getFrom();
  1087. message.to = helper.getTo();
  1088. message.cc = helper.getCc();
  1089. message.bcc = helper.getBcc();
  1090. message.reply = helper.getReply();
  1091. message.subject = imessage.getSubject();
  1092. message.body = helper.getHtml();
  1093. message.received = imessage.getReceivedDate().getTime();
  1094. message.sent = (imessage.getSentDate() == null ? null : imessage.getSentDate().getTime());
  1095. message.seen = seen;
  1096. message.ui_seen = seen;
  1097. message.ui_hide = false;
  1098. message.id = db.message().insertMessage(message);
  1099. Log.i(Helper.TAG, folder.name + " added id=" + message.id + " uid=" + message.uid);
  1100. int sequence = 0;
  1101. for (EntityAttachment attachment : helper.getAttachments()) {
  1102. sequence++;
  1103. Log.i(Helper.TAG, "attachment seq=" + sequence +
  1104. " name=" + attachment.name + " type=" + attachment.type);
  1105. attachment.message = message.id;
  1106. attachment.sequence = sequence;
  1107. attachment.id = db.attachment().insertAttachment(attachment);
  1108. }
  1109. db.setTransactionSuccessful();
  1110. } finally {
  1111. db.endTransaction();
  1112. }
  1113. return 1;
  1114. } finally {
  1115. //Log.v(Helper.TAG, folder.name + " end sync uid=" + uid);
  1116. }
  1117. }
  1118. private class ServiceManager extends ConnectivityManager.NetworkCallback {
  1119. private ServiceState state = new ServiceState();
  1120. private boolean running = false;
  1121. private Thread main;
  1122. private EntityFolder outbox = null;
  1123. private ExecutorService lifecycle = Executors.newSingleThreadExecutor();
  1124. private ExecutorService executor = Executors.newSingleThreadExecutor();
  1125. @Override
  1126. public void onAvailable(Network network) {
  1127. Log.i(Helper.TAG, "Network available " + network);
  1128. if (running)
  1129. Log.i(Helper.TAG, "Service already running");
  1130. else {
  1131. Log.i(Helper.TAG, "Service not running");
  1132. running = true;
  1133. lifecycle.submit(new Runnable() {
  1134. @Override
  1135. public void run() {
  1136. Log.i(Helper.TAG, "Starting service");
  1137. start();
  1138. }
  1139. });
  1140. }
  1141. }
  1142. @Override
  1143. public void onLost(Network network) {
  1144. Log.i(Helper.TAG, "Network lost " + network);
  1145. if (running) {
  1146. Log.i(Helper.TAG, "Service running");
  1147. ConnectivityManager cm = getSystemService(ConnectivityManager.class);
  1148. NetworkInfo ni = cm.getActiveNetworkInfo();
  1149. Log.i(Helper.TAG, "Network active=" + (ni == null ? null : ni.toString()));
  1150. if (ni == null || !ni.isConnected()) {
  1151. Log.i(Helper.TAG, "Network disconnected=" + ni);
  1152. running = false;
  1153. lifecycle.submit(new Runnable() {
  1154. @Override
  1155. public void run() {
  1156. Log.i(Helper.TAG, "Stopping service");
  1157. stop(true);
  1158. }
  1159. });
  1160. }
  1161. } else
  1162. Log.i(Helper.TAG, "Service not running");
  1163. }
  1164. private void start() {
  1165. synchronized (state) {
  1166. state.running = true;
  1167. state.disconnected = false;
  1168. }
  1169. main = new Thread(new Runnable() {
  1170. private List<Thread> threads = new ArrayList<>();
  1171. @Override
  1172. public void run() {
  1173. DB db = DB.getInstance(ServiceSynchronize.this);
  1174. try {
  1175. outbox = db.folder().getOutbox();
  1176. if (outbox == null) {
  1177. Log.i(Helper.TAG, "No outbox, halt");
  1178. stopSelf();
  1179. return;
  1180. }
  1181. List<EntityAccount> accounts = db.account().getAccounts(true);
  1182. if (accounts.size() == 0) {
  1183. Log.i(Helper.TAG, "No accounts, halt");
  1184. stopSelf();
  1185. return;
  1186. }
  1187. // Start monitoring outbox
  1188. IntentFilter f = new IntentFilter(ACTION_PROCESS_OPERATIONS);
  1189. f.addDataType("account/outbox");
  1190. LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(ServiceSynchronize.this);
  1191. lbm.registerReceiver(outboxReceiver, f);
  1192. db.folder().setFolderState(outbox.id, "connected");
  1193. lbm.sendBroadcast(new Intent(ACTION_PROCESS_OPERATIONS)
  1194. .setType("account/outbox")
  1195. .putExtra("folder", outbox.id));
  1196. // Start monitoring accounts
  1197. for (final EntityAccount account : accounts) {
  1198. Log.i(Helper.TAG, account.host + "/" + account.user + " run");
  1199. Thread t = new Thread(new Runnable() {
  1200. @Override
  1201. public void run() {
  1202. try {
  1203. monitorAccount(account, state);
  1204. } catch (Throwable ex) {
  1205. // Fall-safe
  1206. Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  1207. }
  1208. }
  1209. }, "sync.account." + account.id);
  1210. t.start();
  1211. threads.add(t);
  1212. }
  1213. // Stop monitoring accounts
  1214. for (Thread t : threads)
  1215. join(t);
  1216. threads.clear();
  1217. executor.shutdown();
  1218. // Stop monitoring outbox
  1219. lbm.unregisterReceiver(outboxReceiver);
  1220. Log.i(Helper.TAG, outbox.name + " unlisten operations");
  1221. db.folder().setFolderState(outbox.id, null);
  1222. } catch (Throwable ex) {
  1223. // Fail-safe
  1224. Log.e(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  1225. }
  1226. }
  1227. }, "sync.main");
  1228. main.start();
  1229. }
  1230. private void stop(boolean disconnected) {
  1231. if (main != null) {
  1232. synchronized (state) {
  1233. state.running = false;
  1234. state.disconnected = disconnected;
  1235. state.notifyAll();
  1236. }
  1237. // stop wait or backoff
  1238. main.interrupt();
  1239. join(main);
  1240. main = null;
  1241. }
  1242. }
  1243. private BroadcastReceiver outboxReceiver = new BroadcastReceiver() {
  1244. @Override
  1245. public void onReceive(Context context, Intent intent) {
  1246. Log.v(Helper.TAG, outbox.name + " run operations");
  1247. // Create session
  1248. Properties props = MessageHelper.getSessionProperties();
  1249. final Session isession = Session.getInstance(props, null);
  1250. try {
  1251. executor.submit(new Runnable() {
  1252. @Override
  1253. public void run() {
  1254. try {
  1255. Log.v(Helper.TAG, outbox.name + " start operations");
  1256. processOperations(outbox, isession, null, null);
  1257. } catch (Throwable ex) {
  1258. Log.e(Helper.TAG, outbox.name + " " + ex + "\n" + Log.getStackTraceString(ex));
  1259. reportError(null, outbox.name, ex);
  1260. } finally {
  1261. Log.v(Helper.TAG, outbox.name + " end operations");
  1262. }
  1263. }
  1264. });
  1265. } catch (RejectedExecutionException ex) {
  1266. Log.w(Helper.TAG, ex + "\n" + Log.getStackTraceString(ex));
  1267. }
  1268. }
  1269. };
  1270. }
  1271. private static void join(Thread thread) {
  1272. boolean joined = false;
  1273. while (!joined)
  1274. try {
  1275. Log.i(Helper.TAG, "Joining " + thread.getName());
  1276. thread.join();
  1277. joined = true;
  1278. Log.i(Helper.TAG, "Joined " + thread.getName());
  1279. } catch (InterruptedException ex) {
  1280. Log.e(Helper.TAG, thread.getName() + " join " + ex.toString());
  1281. }
  1282. }
  1283. private static void acquire(Semaphore semaphore, String name) {
  1284. boolean acquired = false;
  1285. while (!acquired)
  1286. try {
  1287. semaphore.acquire();
  1288. acquired = true;
  1289. } catch (InterruptedException ex) {
  1290. Log.e(Helper.TAG, name + " acquire " + ex.toString());
  1291. }
  1292. }
  1293. private IBinder binder = new LocalBinder();
  1294. private class LocalBinder extends Binder {
  1295. ServiceSynchronize getService() {
  1296. return ServiceSynchronize.this;
  1297. }
  1298. }
  1299. @Override
  1300. public IBinder onBind(Intent intent) {
  1301. return binder;
  1302. }
  1303. public void quit() {
  1304. Log.i(Helper.TAG, "Service quit");
  1305. serviceManager.stop(false);
  1306. Log.i(Helper.TAG, "Service quited");
  1307. stopSelf();
  1308. }
  1309. public static void start(Context context) {
  1310. ContextCompat.startForegroundService(context, new Intent(context, ServiceSynchronize.class));
  1311. }
  1312. public static void stopSynchronous(Context context, String reason) {
  1313. Log.i(Helper.TAG, "Stop because of '" + reason + "'");
  1314. final Semaphore semaphore = new Semaphore(0, true);
  1315. ServiceConnection connection = new ServiceConnection() {
  1316. @Override
  1317. public void onServiceConnected(ComponentName componentName, IBinder binder) {
  1318. Log.i(Helper.TAG, "Service connected");
  1319. ((LocalBinder) binder).getService().quit();
  1320. semaphore.release();
  1321. }
  1322. @Override
  1323. public void onServiceDisconnected(ComponentName componentName) {
  1324. Log.i(Helper.TAG, "Service disconnected");
  1325. semaphore.release();
  1326. }
  1327. @Override
  1328. public void onBindingDied(ComponentName name) {
  1329. Log.i(Helper.TAG, "Service died");
  1330. semaphore.release();
  1331. }
  1332. };
  1333. Intent intent = new Intent(context, ServiceSynchronize.class);
  1334. boolean exists = context.getApplicationContext().bindService(intent, connection, Context.BIND_AUTO_CREATE);
  1335. Log.i(Helper.TAG, "Service exists=" + exists);
  1336. if (exists) {
  1337. Log.i(Helper.TAG, "Service stopping");
  1338. acquire(semaphore, "service");
  1339. context.getApplicationContext().unbindService(connection);
  1340. }
  1341. Log.i(Helper.TAG, "Service stopped");
  1342. }
  1343. private class ServiceState {
  1344. boolean running = false;
  1345. boolean disconnected = false;
  1346. }
  1347. }