/* * Copyright (c) 2010 Jan Berkel <jan.berkel@gmail.com> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.zegoggles.smssync.mail; import android.content.Context; import android.net.ConnectivityManager; import android.net.Uri; import android.provider.Telephony; import android.text.TextUtils; import android.util.Log; import com.fsck.k9.mail.FetchProfile; import com.fsck.k9.mail.Folder; import com.fsck.k9.mail.Folder.FolderType; import com.fsck.k9.mail.Message; import com.fsck.k9.mail.MessageRetrievalListener; import com.fsck.k9.mail.MessagingException; import com.fsck.k9.mail.ssl.DefaultTrustedSocketFactory; import com.fsck.k9.mail.ssl.TrustedSocketFactory; import com.fsck.k9.mail.store.imap.ImapFolder; import com.fsck.k9.mail.store.imap.ImapMessage; import com.fsck.k9.mail.store.imap.ImapResponse; import com.fsck.k9.mail.store.imap.ImapSearcher; import com.fsck.k9.mail.store.imap.ImapStore; import com.zegoggles.smssync.MmsConsts; import org.jetbrains.annotations.NotNull; import java.io.IOException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import static android.content.Context.CONNECTIVITY_SERVICE; import static com.zegoggles.smssync.App.LOCAL_LOGV; import static com.zegoggles.smssync.App.TAG; import static java.util.Collections.sort; public class BackupImapStore extends ImapStore { private final Context context; private final Map<DataType, BackupFolder> openFolders = new HashMap<DataType, BackupFolder>(); public BackupImapStore(final Context context, final String uri) throws MessagingException { super(new BackupStoreConfig(uri), getTrustedSocketFactory(context, uri), (ConnectivityManager) context.getSystemService(CONNECTIVITY_SERVICE)); this.context = context; } public BackupFolder getFolder(DataType type) throws MessagingException { BackupFolder folder = openFolders.get(type); if (folder == null) { String label = type.getFolder(context); if (label == null) throw new IllegalStateException("label is null"); folder = createAndOpenFolder(type, label); openFolders.put(type, folder); } return folder; } public void closeFolders() { Collection<BackupFolder> folders = openFolders.values(); for (BackupFolder folder : folders) { try { folder.close(); } catch (Exception e) { Log.w(TAG, e); } } openFolders.clear(); } @Override public String toString() { return "BackupImapStore{" + "uri=" + getStoreUriForLogging() + '}'; } /** * @return a uri which can be used for logging (i.e. with credentials masked) */ public String getStoreUriForLogging() { Uri uri = Uri.parse(this.mStoreConfig.getStoreUri()); String userInfo = uri.getUserInfo(); if (!TextUtils.isEmpty(userInfo) && userInfo.contains(":")) { String[] parts = userInfo.split(":", 2); userInfo = parts[0]+":"+(parts[1].replaceAll(".", "X")); String host = uri.getHost(); if (uri.getPort() != -1) { host += ":"+uri.getPort(); } return uri.buildUpon().encodedAuthority(userInfo + "@" + host).toString(); } else { return uri.toString(); } } /* package, for testing */ TrustedSocketFactory getTrustedSocketFactory() { return mTrustedSocketFactory; } private @NotNull BackupFolder createAndOpenFolder(DataType type, @NotNull String label) throws MessagingException { try { BackupFolder folder = new BackupFolder(this, label, type); if (!folder.exists()) { Log.i(TAG, "Label '" + label + "' does not exist yet. Creating."); folder.create(FolderType.HOLDS_MESSAGES); } folder.open(Folder.OPEN_MODE_RW); return folder; } catch (IllegalArgumentException e) { // thrown inside K9 Log.e(TAG, "K9 error", e); throw new MessagingException(e.getMessage()); } } public String getStoreUri() { return mStoreConfig.getStoreUri(); } public class BackupFolder extends ImapFolder { private final DataType type; public BackupFolder(ImapStore store, String name, DataType type) { super(store, name); this.type = type; } public List<ImapMessage> getMessages(final int max, final boolean flagged, final Date since) throws MessagingException { if (LOCAL_LOGV) Log.v(TAG, String.format(Locale.ENGLISH, "getMessages(%d, %b, %s)", max, flagged, since)); final List<ImapMessage> messages; final ImapSearcher searcher = new ImapSearcher() { @Override public List<ImapResponse> search() throws IOException, MessagingException { final StringBuilder sb = new StringBuilder("UID SEARCH 1:*") .append(' ') .append(getQuery()) .append(" UNDELETED"); if (since != null) sb.append(" SENTSINCE ").append(RFC3501_DATE.get().format(since)); if (flagged) sb.append(" FLAGGED"); return executeSimpleCommand(sb.toString().trim()); } }; final List<ImapMessage> msgs = search(searcher, null); Log.i(TAG, "Found " + msgs.size() + " msgs" + (since == null ? "" : " (since " + since + ")")); if (max > 0 && msgs.size() > max) { if (LOCAL_LOGV) Log.v(TAG, "Fetching envelopes"); FetchProfile fp = new FetchProfile(); fp.add(FetchProfile.Item.DATE); fetch(msgs, fp, null); if (LOCAL_LOGV) Log.v(TAG, "Sorting"); //Debug.startMethodTracing("sorting"); sort(msgs, MessageComparator.INSTANCE); //Debug.stopMethodTracing(); if (LOCAL_LOGV) Log.v(TAG, "Sorting done"); messages = new ArrayList<ImapMessage>(max); messages.addAll(msgs.subList(0, max)); } else { messages = msgs; } Collections.reverse(messages); return messages; } private String getQuery() { switch (this.type) { /* MMS/SMS are special cases since we need to support legacy backup headers */ case SMS: return String.format(Locale.ENGLISH, "(OR HEADER %s \"%s\" (NOT HEADER %s \"\" (OR HEADER %s \"%d\" HEADER %s \"%d\")))", Headers.DATATYPE.toUpperCase(Locale.ENGLISH), type, Headers.DATATYPE.toUpperCase(Locale.ENGLISH), Headers.TYPE.toUpperCase(Locale.ENGLISH), Telephony.TextBasedSmsColumns.MESSAGE_TYPE_INBOX, Headers.TYPE.toUpperCase(Locale.ENGLISH), Telephony.TextBasedSmsColumns.MESSAGE_TYPE_SENT); case MMS: return String.format(Locale.ENGLISH, "(OR HEADER %s \"%s\" (NOT HEADER %s \"\" HEADER %s \"%s\"))", Headers.DATATYPE.toUpperCase(Locale.ENGLISH), type, Headers.DATATYPE.toUpperCase(Locale.ENGLISH), Headers.TYPE.toUpperCase(Locale.ENGLISH), MmsConsts.LEGACY_HEADER); default: return String.format(Locale.ENGLISH, "(HEADER %s \"%s\")", Headers.DATATYPE.toUpperCase(Locale.ENGLISH), type); } } // TODO should not have to override these methods, but mockito fails to generate working mocks otherwise :/ @Override public boolean equals(Object o) { return super.equals(o); } @Override public void fetch(List<ImapMessage> messages, FetchProfile fp, MessageRetrievalListener<ImapMessage> listener) throws MessagingException { super.fetch(messages, fp, listener); } @Override public Map<String, String> appendMessages(List<? extends Message> messages) throws MessagingException { return super.appendMessages(messages); } } static class MessageComparator implements Comparator<Message> { static final MessageComparator INSTANCE = new MessageComparator(); static final Date EARLY = new Date(0); public int compare(final Message m1, final Message m2) { final Date d1 = m1 == null ? EARLY : m1.getSentDate() != null ? m1.getSentDate() : EARLY; final Date d2 = m2 == null ? EARLY : m2.getSentDate() != null ? m2.getSentDate() : EARLY; return d2.compareTo(d1); } } public static boolean isValidImapFolder(String imapFolder) { return !(imapFolder == null || imapFolder.length() == 0) && !(imapFolder.charAt(0) == '/' || imapFolder.charAt(0) == ' ' || imapFolder.charAt(imapFolder.length() - 1) == ' '); } public static boolean isValidUri(String uri) { if (TextUtils.isEmpty(uri)) return false; Uri parsed = Uri.parse(uri); return parsed != null && !TextUtils.isEmpty(parsed.getAuthority()) && !TextUtils.isEmpty(parsed.getHost()) && !TextUtils.isEmpty(parsed.getScheme()) && ("imap+ssl+".equalsIgnoreCase(parsed.getScheme()) || "imap+ssl".equalsIgnoreCase(parsed.getScheme()) || "imap".equalsIgnoreCase(parsed.getScheme()) || "imap+tls+".equalsIgnoreCase(parsed.getScheme()) || "imap+tls".equalsIgnoreCase(parsed.getScheme())); } // reimplement trust-all logic which was removed in // https://github.com/k9mail/k-9/commit/daea7f1ecdb4515298a6c57dd5a829689426c2c9 private static TrustedSocketFactory getTrustedSocketFactory(Context context, String storeUri) { try { if (isInsecureStoreUri(new URI(storeUri))) { Log.d(TAG, "insecure store uri specified, trusting ALL certificates"); return AllTrustedSocketFactory.INSTANCE; } } catch (URISyntaxException ignored) { } return new DefaultTrustedSocketFactory(context); } private static boolean isInsecureStoreUri(URI uri) { return "imap+tls".equals(uri.getScheme()) || "imap+ssl".equals(uri.getScheme()); } }