mirror of
https://github.com/moparisthebest/k-9
synced 2024-11-27 03:32:16 -05:00
merge extra crap
This commit is contained in:
parent
9cf84ee2c8
commit
d55e335f48
@ -1,408 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Account stores all of the settings for a single account defined by the user. It is able to save
|
|
||||||
* and delete itself given a Preferences to work with. Each account is defined by a UUID.
|
|
||||||
*/
|
|
||||||
public class Account implements Serializable {
|
|
||||||
public static final int DELETE_POLICY_NEVER = 0;
|
|
||||||
public static final int DELETE_POLICY_7DAYS = 1;
|
|
||||||
public static final int DELETE_POLICY_ON_DELETE = 2;
|
|
||||||
|
|
||||||
private static final long serialVersionUID = 2975156672298625121L;
|
|
||||||
|
|
||||||
String mUuid;
|
|
||||||
String mStoreUri;
|
|
||||||
String mLocalStoreUri;
|
|
||||||
String mTransportUri;
|
|
||||||
String mDescription;
|
|
||||||
String mName;
|
|
||||||
String mEmail;
|
|
||||||
String mSignature;
|
|
||||||
String mAlwaysBcc;
|
|
||||||
int mAutomaticCheckIntervalMinutes;
|
|
||||||
int mDisplayCount;
|
|
||||||
long mLastAutomaticCheckTime;
|
|
||||||
boolean mNotifyNewMail;
|
|
||||||
boolean mNotifyRingtone;
|
|
||||||
String mDraftsFolderName;
|
|
||||||
String mSentFolderName;
|
|
||||||
String mTrashFolderName;
|
|
||||||
String mOutboxFolderName;
|
|
||||||
int mAccountNumber;
|
|
||||||
boolean mVibrate;
|
|
||||||
String mRingtoneUri;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <pre>
|
|
||||||
* 0 Never
|
|
||||||
* 1 After 7 days
|
|
||||||
* 2 When I delete from inbox
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
int mDeletePolicy;
|
|
||||||
|
|
||||||
public Account(Context context) {
|
|
||||||
// TODO Change local store path to something readable / recognizable
|
|
||||||
mUuid = UUID.randomUUID().toString();
|
|
||||||
mLocalStoreUri = "local://localhost/" + context.getDatabasePath(mUuid + ".db");
|
|
||||||
mAutomaticCheckIntervalMinutes = -1;
|
|
||||||
mDisplayCount = -1;
|
|
||||||
mAccountNumber = -1;
|
|
||||||
mNotifyNewMail = true;
|
|
||||||
mNotifyRingtone = false;
|
|
||||||
mSignature = "Sent from my Android phone with K-9. Please excuse my brevity.";
|
|
||||||
mVibrate = false;
|
|
||||||
mRingtoneUri = "content://settings/system/notification_sound";
|
|
||||||
}
|
|
||||||
|
|
||||||
Account(Preferences preferences, String uuid) {
|
|
||||||
this.mUuid = uuid;
|
|
||||||
refresh(preferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Refresh the account from the stored settings.
|
|
||||||
*/
|
|
||||||
public void refresh(Preferences preferences) {
|
|
||||||
mStoreUri = Utility.base64Decode(preferences.mSharedPreferences.getString(mUuid
|
|
||||||
+ ".storeUri", null));
|
|
||||||
mLocalStoreUri = preferences.mSharedPreferences.getString(mUuid + ".localStoreUri", null);
|
|
||||||
mTransportUri = Utility.base64Decode(preferences.mSharedPreferences.getString(mUuid
|
|
||||||
+ ".transportUri", null));
|
|
||||||
mDescription = preferences.mSharedPreferences.getString(mUuid + ".description", null);
|
|
||||||
mAlwaysBcc = preferences.mSharedPreferences.getString(mUuid + ".alwaysBcc", mAlwaysBcc);
|
|
||||||
mName = preferences.mSharedPreferences.getString(mUuid + ".name", mName);
|
|
||||||
mEmail = preferences.mSharedPreferences.getString(mUuid + ".email", mEmail);
|
|
||||||
mSignature = preferences.mSharedPreferences.getString(mUuid + ".signature", mSignature);
|
|
||||||
mAutomaticCheckIntervalMinutes = preferences.mSharedPreferences.getInt(mUuid
|
|
||||||
+ ".automaticCheckIntervalMinutes", -1);
|
|
||||||
mDisplayCount = preferences.mSharedPreferences.getInt(mUuid + ".displayCount", -1);
|
|
||||||
mLastAutomaticCheckTime = preferences.mSharedPreferences.getLong(mUuid
|
|
||||||
+ ".lastAutomaticCheckTime", 0);
|
|
||||||
mNotifyNewMail = preferences.mSharedPreferences.getBoolean(mUuid + ".notifyNewMail",
|
|
||||||
false);
|
|
||||||
mNotifyRingtone = preferences.mSharedPreferences.getBoolean(mUuid + ".notifyRingtone",
|
|
||||||
false);
|
|
||||||
mDeletePolicy = preferences.mSharedPreferences.getInt(mUuid + ".deletePolicy", 0);
|
|
||||||
mDraftsFolderName = preferences.mSharedPreferences.getString(mUuid + ".draftsFolderName",
|
|
||||||
"Drafts");
|
|
||||||
mSentFolderName = preferences.mSharedPreferences.getString(mUuid + ".sentFolderName",
|
|
||||||
"Sent");
|
|
||||||
mTrashFolderName = preferences.mSharedPreferences.getString(mUuid + ".trashFolderName",
|
|
||||||
"Trash");
|
|
||||||
mOutboxFolderName = preferences.mSharedPreferences.getString(mUuid + ".outboxFolderName",
|
|
||||||
"Outbox");
|
|
||||||
mAccountNumber = preferences.mSharedPreferences.getInt(mUuid + ".accountNumber", 0);
|
|
||||||
mVibrate = preferences.mSharedPreferences.getBoolean(mUuid + ".vibrate", false);
|
|
||||||
mRingtoneUri = preferences.mSharedPreferences.getString(mUuid + ".ringtone",
|
|
||||||
"content://settings/system/notification_sound");
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getUuid() {
|
|
||||||
return mUuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getStoreUri() {
|
|
||||||
return mStoreUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStoreUri(String storeUri) {
|
|
||||||
this.mStoreUri = storeUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTransportUri() {
|
|
||||||
return mTransportUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTransportUri(String transportUri) {
|
|
||||||
this.mTransportUri = transportUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDescription() {
|
|
||||||
return mDescription;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDescription(String description) {
|
|
||||||
this.mDescription = description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getName() {
|
|
||||||
return mName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setName(String name) {
|
|
||||||
this.mName = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSignature() {
|
|
||||||
return mSignature;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSignature(String signature) {
|
|
||||||
this.mSignature = signature;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getEmail() {
|
|
||||||
return mEmail;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEmail(String email) {
|
|
||||||
this.mEmail = email;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAlwaysBcc() {
|
|
||||||
return mAlwaysBcc;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAlwaysBcc(String alwaysBcc) {
|
|
||||||
this.mAlwaysBcc = alwaysBcc;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean isVibrate() {
|
|
||||||
return mVibrate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setVibrate(boolean vibrate) {
|
|
||||||
mVibrate = vibrate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getRingtone() {
|
|
||||||
return mRingtoneUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRingtone(String ringtoneUri) {
|
|
||||||
mRingtoneUri = ringtoneUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete(Preferences preferences) {
|
|
||||||
String[] uuids = preferences.mSharedPreferences.getString("accountUuids", "").split(",");
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
for (int i = 0, length = uuids.length; i < length; i++) {
|
|
||||||
if (!uuids[i].equals(mUuid)) {
|
|
||||||
if (sb.length() > 0) {
|
|
||||||
sb.append(',');
|
|
||||||
}
|
|
||||||
sb.append(uuids[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String accountUuids = sb.toString();
|
|
||||||
SharedPreferences.Editor editor = preferences.mSharedPreferences.edit();
|
|
||||||
editor.putString("accountUuids", accountUuids);
|
|
||||||
|
|
||||||
editor.remove(mUuid + ".storeUri");
|
|
||||||
editor.remove(mUuid + ".localStoreUri");
|
|
||||||
editor.remove(mUuid + ".transportUri");
|
|
||||||
editor.remove(mUuid + ".description");
|
|
||||||
editor.remove(mUuid + ".name");
|
|
||||||
editor.remove(mUuid + ".email");
|
|
||||||
editor.remove(mUuid + ".alwaysBcc");
|
|
||||||
editor.remove(mUuid + ".automaticCheckIntervalMinutes");
|
|
||||||
editor.remove(mUuid + ".lastAutomaticCheckTime");
|
|
||||||
editor.remove(mUuid + ".notifyNewMail");
|
|
||||||
editor.remove(mUuid + ".deletePolicy");
|
|
||||||
editor.remove(mUuid + ".draftsFolderName");
|
|
||||||
editor.remove(mUuid + ".sentFolderName");
|
|
||||||
editor.remove(mUuid + ".trashFolderName");
|
|
||||||
editor.remove(mUuid + ".outboxFolderName");
|
|
||||||
editor.remove(mUuid + ".accountNumber");
|
|
||||||
editor.remove(mUuid + ".vibrate");
|
|
||||||
editor.remove(mUuid + ".ringtone");
|
|
||||||
editor.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save(Preferences preferences) {
|
|
||||||
if (!preferences.mSharedPreferences.getString("accountUuids", "").contains(mUuid)) {
|
|
||||||
/*
|
|
||||||
* When the account is first created we assign it a unique account number. The
|
|
||||||
* account number will be unique to that account for the lifetime of the account.
|
|
||||||
* So, we get all the existing account numbers, sort them ascending, loop through
|
|
||||||
* the list and check if the number is greater than 1 + the previous number. If so
|
|
||||||
* we use the previous number + 1 as the account number. This refills gaps.
|
|
||||||
* mAccountNumber starts as -1 on a newly created account. It must be -1 for this
|
|
||||||
* algorithm to work.
|
|
||||||
*
|
|
||||||
* I bet there is a much smarter way to do this. Anyone like to suggest it?
|
|
||||||
*/
|
|
||||||
Account[] accounts = preferences.getAccounts();
|
|
||||||
int[] accountNumbers = new int[accounts.length];
|
|
||||||
for (int i = 0; i < accounts.length; i++) {
|
|
||||||
accountNumbers[i] = accounts[i].getAccountNumber();
|
|
||||||
}
|
|
||||||
Arrays.sort(accountNumbers);
|
|
||||||
for (int accountNumber : accountNumbers) {
|
|
||||||
if (accountNumber > mAccountNumber + 1) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
mAccountNumber = accountNumber;
|
|
||||||
}
|
|
||||||
mAccountNumber++;
|
|
||||||
|
|
||||||
String accountUuids = preferences.mSharedPreferences.getString("accountUuids", "");
|
|
||||||
accountUuids += (accountUuids.length() != 0 ? "," : "") + mUuid;
|
|
||||||
SharedPreferences.Editor editor = preferences.mSharedPreferences.edit();
|
|
||||||
editor.putString("accountUuids", accountUuids);
|
|
||||||
editor.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
SharedPreferences.Editor editor = preferences.mSharedPreferences.edit();
|
|
||||||
|
|
||||||
editor.putString(mUuid + ".storeUri", Utility.base64Encode(mStoreUri));
|
|
||||||
editor.putString(mUuid + ".localStoreUri", mLocalStoreUri);
|
|
||||||
editor.putString(mUuid + ".transportUri", Utility.base64Encode(mTransportUri));
|
|
||||||
editor.putString(mUuid + ".description", mDescription);
|
|
||||||
editor.putString(mUuid + ".name", mName);
|
|
||||||
editor.putString(mUuid + ".email", mEmail);
|
|
||||||
editor.putString(mUuid + ".signature", mSignature);
|
|
||||||
editor.putString(mUuid + ".alwaysBcc", mAlwaysBcc);
|
|
||||||
editor.putInt(mUuid + ".automaticCheckIntervalMinutes", mAutomaticCheckIntervalMinutes);
|
|
||||||
editor.putInt(mUuid + ".displayCount", mDisplayCount);
|
|
||||||
editor.putLong(mUuid + ".lastAutomaticCheckTime", mLastAutomaticCheckTime);
|
|
||||||
editor.putBoolean(mUuid + ".notifyNewMail", mNotifyNewMail);
|
|
||||||
editor.putBoolean(mUuid + ".notifyRingtone", mNotifyRingtone);
|
|
||||||
editor.putInt(mUuid + ".deletePolicy", mDeletePolicy);
|
|
||||||
editor.putString(mUuid + ".draftsFolderName", mDraftsFolderName);
|
|
||||||
editor.putString(mUuid + ".sentFolderName", mSentFolderName);
|
|
||||||
editor.putString(mUuid + ".trashFolderName", mTrashFolderName);
|
|
||||||
editor.putString(mUuid + ".outboxFolderName", mOutboxFolderName);
|
|
||||||
editor.putInt(mUuid + ".accountNumber", mAccountNumber);
|
|
||||||
editor.putBoolean(mUuid + ".vibrate", mVibrate);
|
|
||||||
editor.putString(mUuid + ".ringtone", mRingtoneUri);
|
|
||||||
editor.commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return mDescription;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Uri getContentUri() {
|
|
||||||
return Uri.parse("content://accounts/" + getUuid());
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getLocalStoreUri() {
|
|
||||||
return mLocalStoreUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLocalStoreUri(String localStoreUri) {
|
|
||||||
this.mLocalStoreUri = localStoreUri;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns -1 for never.
|
|
||||||
*/
|
|
||||||
public int getAutomaticCheckIntervalMinutes() {
|
|
||||||
return mAutomaticCheckIntervalMinutes;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDisplayCount() {
|
|
||||||
if (mDisplayCount == -1) {
|
|
||||||
this.mDisplayCount = k9.DEFAULT_VISIBLE_LIMIT;
|
|
||||||
}
|
|
||||||
return mDisplayCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param automaticCheckIntervalMinutes or -1 for never.
|
|
||||||
*/
|
|
||||||
public void setAutomaticCheckIntervalMinutes(int automaticCheckIntervalMinutes) {
|
|
||||||
this.mAutomaticCheckIntervalMinutes = automaticCheckIntervalMinutes;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param displayCount
|
|
||||||
*/
|
|
||||||
public void setDisplayCount(int displayCount) {
|
|
||||||
if (displayCount != -1) {
|
|
||||||
this.mDisplayCount = displayCount;
|
|
||||||
} else {
|
|
||||||
this.mDisplayCount = k9.DEFAULT_VISIBLE_LIMIT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getLastAutomaticCheckTime() {
|
|
||||||
return mLastAutomaticCheckTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setLastAutomaticCheckTime(long lastAutomaticCheckTime) {
|
|
||||||
this.mLastAutomaticCheckTime = lastAutomaticCheckTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isNotifyRingtone() {
|
|
||||||
return mNotifyRingtone;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setNotifyRingtone(boolean notifyRingtone) {
|
|
||||||
this.mNotifyRingtone = notifyRingtone;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
public boolean isNotifyNewMail() {
|
|
||||||
return mNotifyNewMail;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setNotifyNewMail(boolean notifyNewMail) {
|
|
||||||
this.mNotifyNewMail = notifyNewMail;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDeletePolicy() {
|
|
||||||
return mDeletePolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDeletePolicy(int deletePolicy) {
|
|
||||||
this.mDeletePolicy = deletePolicy;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDraftsFolderName() {
|
|
||||||
return mDraftsFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDraftsFolderName(String draftsFolderName) {
|
|
||||||
mDraftsFolderName = draftsFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getSentFolderName() {
|
|
||||||
return mSentFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSentFolderName(String sentFolderName) {
|
|
||||||
mSentFolderName = sentFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getTrashFolderName() {
|
|
||||||
return mTrashFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setTrashFolderName(String trashFolderName) {
|
|
||||||
mTrashFolderName = trashFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getOutboxFolderName() {
|
|
||||||
return mOutboxFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOutboxFolderName(String outboxFolderName) {
|
|
||||||
mOutboxFolderName = outboxFolderName;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getAccountNumber() {
|
|
||||||
return mAccountNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (o instanceof Account) {
|
|
||||||
return ((Account)o).mUuid.equals(mUuid);
|
|
||||||
}
|
|
||||||
return super.equals(o);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,88 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (C) 2007 The Android Open Source Project
|
|
||||||
*
|
|
||||||
* 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.fsck.k9;
|
|
||||||
|
|
||||||
import static android.provider.Contacts.ContactMethods.CONTENT_EMAIL_URI;
|
|
||||||
import android.content.ContentResolver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.DatabaseUtils;
|
|
||||||
import android.provider.Contacts.ContactMethods;
|
|
||||||
import android.provider.Contacts.People;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ResourceCursorAdapter;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.fsck.k9.mail.Address;
|
|
||||||
|
|
||||||
public class EmailAddressAdapter extends ResourceCursorAdapter {
|
|
||||||
public static final int NAME_INDEX = 1;
|
|
||||||
|
|
||||||
public static final int DATA_INDEX = 2;
|
|
||||||
|
|
||||||
private static final String SORT_ORDER = People.TIMES_CONTACTED + " DESC, " + People.NAME;
|
|
||||||
|
|
||||||
private ContentResolver mContentResolver;
|
|
||||||
|
|
||||||
private static final String[] PROJECTION = {
|
|
||||||
ContactMethods._ID, // 0
|
|
||||||
ContactMethods.NAME, // 1
|
|
||||||
ContactMethods.DATA
|
|
||||||
// 2
|
|
||||||
};
|
|
||||||
|
|
||||||
public EmailAddressAdapter(Context context) {
|
|
||||||
super(context, R.layout.recipient_dropdown_item, null);
|
|
||||||
mContentResolver = context.getContentResolver();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final String convertToString(Cursor cursor) {
|
|
||||||
String name = cursor.getString(NAME_INDEX);
|
|
||||||
String address = cursor.getString(DATA_INDEX);
|
|
||||||
|
|
||||||
return new Address(address, name).toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public final void bindView(View view, Context context, Cursor cursor) {
|
|
||||||
TextView text1 = (TextView)view.findViewById(R.id.text1);
|
|
||||||
TextView text2 = (TextView)view.findViewById(R.id.text2);
|
|
||||||
text1.setText(cursor.getString(NAME_INDEX));
|
|
||||||
text2.setText(cursor.getString(DATA_INDEX));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
|
|
||||||
String where = null;
|
|
||||||
|
|
||||||
if (constraint != null) {
|
|
||||||
String filter = DatabaseUtils.sqlEscapeString(constraint.toString() + '%');
|
|
||||||
|
|
||||||
StringBuilder s = new StringBuilder();
|
|
||||||
s.append("(people.name LIKE ");
|
|
||||||
s.append(filter);
|
|
||||||
s.append(") OR (contact_methods.data LIKE ");
|
|
||||||
s.append(filter);
|
|
||||||
s.append(")");
|
|
||||||
|
|
||||||
where = s.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return mContentResolver.query(CONTENT_EMAIL_URI, PROJECTION, where, null, SORT_ORDER);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,18 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9;
|
|
||||||
|
|
||||||
import com.fsck.k9.mail.Address;
|
|
||||||
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.widget.AutoCompleteTextView.Validator;
|
|
||||||
|
|
||||||
public class EmailAddressValidator implements Validator {
|
|
||||||
public CharSequence fixText(CharSequence invalidText) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isValid(CharSequence text) {
|
|
||||||
return Address.parse(text.toString()).length > 0;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,60 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A filtering InputStream that stops allowing reads after the given length has been read. This
|
|
||||||
* is used to allow a client to read directly from an underlying protocol stream without reading
|
|
||||||
* past where the protocol handler intended the client to read.
|
|
||||||
*/
|
|
||||||
public class FixedLengthInputStream extends InputStream {
|
|
||||||
private InputStream mIn;
|
|
||||||
private int mLength;
|
|
||||||
private int mCount;
|
|
||||||
|
|
||||||
public FixedLengthInputStream(InputStream in, int length) {
|
|
||||||
this.mIn = in;
|
|
||||||
this.mLength = length;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int available() throws IOException {
|
|
||||||
return mLength - mCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
if (mCount < mLength) {
|
|
||||||
mCount++;
|
|
||||||
return mIn.read();
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b, int offset, int length) throws IOException {
|
|
||||||
if (mCount < mLength) {
|
|
||||||
int d = mIn.read(b, offset, Math.min(mLength - mCount, length));
|
|
||||||
if (d == -1) {
|
|
||||||
return -1;
|
|
||||||
} else {
|
|
||||||
mCount += d;
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b) throws IOException {
|
|
||||||
return read(b, 0, b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
/* AUTO-GENERATED FILE. DO NOT MODIFY.
|
|
||||||
*
|
|
||||||
* This class was automatically generated by the
|
|
||||||
* aapt tool from the resource data it found. It
|
|
||||||
* should not be modified by hand.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.fsck.k9;
|
|
||||||
|
|
||||||
public final class Manifest {
|
|
||||||
public static final class permission {
|
|
||||||
public static final String READ_ATTACHMENT="com.fsck.k9.permission.READ_ATTACHMENT";
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,132 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
import com.fsck.k9.mail.Folder;
|
|
||||||
import com.fsck.k9.mail.Message;
|
|
||||||
import com.fsck.k9.mail.Part;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Defines the interface that MessagingController will use to callback to requesters. This class
|
|
||||||
* is defined as non-abstract so that someone who wants to receive only a few messages can
|
|
||||||
* do so without implementing the entire interface. It is highly recommended that users of
|
|
||||||
* this interface use the @Override annotation in their implementations to avoid being caught by
|
|
||||||
* changes in this class.
|
|
||||||
*/
|
|
||||||
public class MessagingListener {
|
|
||||||
public void listFoldersStarted(Account account) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void listFolders(Account account, Folder[] folders) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void listFoldersFailed(Account account, String message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void listFoldersFinished(Account account) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void listLocalMessagesStarted(Account account, String folder) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void listLocalMessages(Account account, String folder, Message[] messages) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void listLocalMessagesFailed(Account account, String folder, String message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void listLocalMessagesFinished(Account account, String folder) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void synchronizeMailboxStarted(Account account, String folder) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void synchronizeMailboxNewMessage(Account account, String folder, Message message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void synchronizeMailboxRemovedMessage(Account account, String folder,Message message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void synchronizeMailboxFinished(Account account, String folder,
|
|
||||||
int totalMessagesInMailbox, int numNewMessages) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void synchronizeMailboxFailed(Account account, String folder,
|
|
||||||
String message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadMessageForViewStarted(Account account, String folder, String uid) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadMessageForViewHeadersAvailable(Account account, String folder, String uid,
|
|
||||||
Message message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadMessageForViewBodyAvailable(Account account, String folder, String uid,
|
|
||||||
Message message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadMessageForViewFinished(Account account, String folder, String uid,
|
|
||||||
Message message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadMessageForViewFailed(Account account, String folder, String uid, String message) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkMailStarted(Context context, Account account) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkMailFinished(Context context, Account account) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkMailFailed(Context context, Account account, String reason) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendPendingMessagesCompleted(Account account) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void emptyTrashCompleted(Account account) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void messageUidChanged(Account account, String folder, String oldUid, String newUid) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadAttachmentStarted(
|
|
||||||
Account account,
|
|
||||||
Message message,
|
|
||||||
Part part,
|
|
||||||
Object tag,
|
|
||||||
boolean requiresDownload)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadAttachmentFinished(
|
|
||||||
Account account,
|
|
||||||
Message message,
|
|
||||||
Part part,
|
|
||||||
Object tag)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public void loadAttachmentFailed(
|
|
||||||
Account account,
|
|
||||||
Message message,
|
|
||||||
Part part,
|
|
||||||
Object tag,
|
|
||||||
String reason)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* General notification messages subclasses can override to be notified that the controller
|
|
||||||
* has completed a command. This is useful for turning off progress indicators that may have
|
|
||||||
* been left over from previous commands.
|
|
||||||
* @param moreCommandsToRun True if the controller will continue on to another command
|
|
||||||
* immediately.
|
|
||||||
*/
|
|
||||||
public void controllerCommandCompleted(boolean moreCommandsToRun) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,64 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A filtering InputStream that allows single byte "peeks" without consuming the byte. The
|
|
||||||
* client of this stream can call peek() to see the next available byte in the stream
|
|
||||||
* and a subsequent read will still return the peeked byte.
|
|
||||||
*/
|
|
||||||
public class PeekableInputStream extends InputStream {
|
|
||||||
private InputStream mIn;
|
|
||||||
private boolean mPeeked;
|
|
||||||
private int mPeekedByte;
|
|
||||||
|
|
||||||
public PeekableInputStream(InputStream in) {
|
|
||||||
this.mIn = in;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
if (!mPeeked) {
|
|
||||||
return mIn.read();
|
|
||||||
} else {
|
|
||||||
mPeeked = false;
|
|
||||||
return mPeekedByte;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int peek() throws IOException {
|
|
||||||
if (!mPeeked) {
|
|
||||||
mPeekedByte = read();
|
|
||||||
mPeeked = true;
|
|
||||||
}
|
|
||||||
return mPeekedByte;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b, int offset, int length) throws IOException {
|
|
||||||
if (!mPeeked) {
|
|
||||||
return mIn.read(b, offset, length);
|
|
||||||
} else {
|
|
||||||
b[0] = (byte)mPeekedByte;
|
|
||||||
mPeeked = false;
|
|
||||||
int r = mIn.read(b, offset + 1, length - 1);
|
|
||||||
if (r == -1) {
|
|
||||||
return 1;
|
|
||||||
} else {
|
|
||||||
return r + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read(byte[] b) throws IOException {
|
|
||||||
return read(b, 0, b.length);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return String.format("PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)",
|
|
||||||
mIn.toString(), mPeeked, mPeekedByte);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,123 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9;
|
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public class Preferences {
|
|
||||||
private static Preferences preferences;
|
|
||||||
|
|
||||||
SharedPreferences mSharedPreferences;
|
|
||||||
|
|
||||||
private Preferences(Context context) {
|
|
||||||
mSharedPreferences = context.getSharedPreferences("AndroidMail.Main", Context.MODE_PRIVATE);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO need to think about what happens if this gets GCed along with the
|
|
||||||
* Activity that initialized it. Do we lose ability to read Preferences in
|
|
||||||
* further Activities? Maybe this should be stored in the Application
|
|
||||||
* context.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static synchronized Preferences getPreferences(Context context) {
|
|
||||||
if (preferences == null) {
|
|
||||||
preferences = new Preferences(context);
|
|
||||||
}
|
|
||||||
return preferences;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of the accounts on the system. If no accounts are
|
|
||||||
* registered the method returns an empty array.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Account[] getAccounts() {
|
|
||||||
String accountUuids = mSharedPreferences.getString("accountUuids", null);
|
|
||||||
if (accountUuids == null || accountUuids.length() == 0) {
|
|
||||||
return new Account[] {};
|
|
||||||
}
|
|
||||||
String[] uuids = accountUuids.split(",");
|
|
||||||
Account[] accounts = new Account[uuids.length];
|
|
||||||
for (int i = 0, length = uuids.length; i < length; i++) {
|
|
||||||
accounts[i] = new Account(this, uuids[i]);
|
|
||||||
}
|
|
||||||
return accounts;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Account getAccountByContentUri(Uri uri) {
|
|
||||||
return new Account(this, uri.getPath().substring(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Account marked as default. If no account is marked as default
|
|
||||||
* the first account in the list is marked as default and then returned. If
|
|
||||||
* there are no accounts on the system the method returns null.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public Account getDefaultAccount() {
|
|
||||||
String defaultAccountUuid = mSharedPreferences.getString("defaultAccountUuid", null);
|
|
||||||
Account defaultAccount = null;
|
|
||||||
Account[] accounts = getAccounts();
|
|
||||||
if (defaultAccountUuid != null) {
|
|
||||||
for (Account account : accounts) {
|
|
||||||
if (account.getUuid().equals(defaultAccountUuid)) {
|
|
||||||
defaultAccount = account;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (defaultAccount == null) {
|
|
||||||
if (accounts.length > 0) {
|
|
||||||
defaultAccount = accounts[0];
|
|
||||||
setDefaultAccount(defaultAccount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return defaultAccount;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDefaultAccount(Account account) {
|
|
||||||
mSharedPreferences.edit().putString("defaultAccountUuid", account.getUuid()).commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEnableDebugLogging(boolean value) {
|
|
||||||
mSharedPreferences.edit().putBoolean("enableDebugLogging", value).commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean geteEnableDebugLogging() {
|
|
||||||
return mSharedPreferences.getBoolean("enableDebugLogging", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEnableSensitiveLogging(boolean value) {
|
|
||||||
mSharedPreferences.edit().putBoolean("enableSensitiveLogging", value).commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean getEnableSensitiveLogging() {
|
|
||||||
return mSharedPreferences.getBoolean("enableSensitiveLogging", false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void save() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
mSharedPreferences.edit().clear().commit();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void dump() {
|
|
||||||
if (Config.LOGV) {
|
|
||||||
for (String key : mSharedPreferences.getAll().keySet()) {
|
|
||||||
Log.v(k9.LOG_TAG, key + " = " + mSharedPreferences.getAll().get(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,176 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import com.fsck.k9.codec.binary.Base64;
|
|
||||||
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
public class Utility {
|
|
||||||
public final static String readInputStream(InputStream in, String encoding) throws IOException {
|
|
||||||
InputStreamReader reader = new InputStreamReader(in, encoding);
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
int count;
|
|
||||||
char[] buf = new char[512];
|
|
||||||
while ((count = reader.read(buf)) != -1) {
|
|
||||||
sb.append(buf, 0, count);
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public final static boolean arrayContains(Object[] a, Object o) {
|
|
||||||
for (int i = 0, count = a.length; i < count; i++) {
|
|
||||||
if (a[i].equals(o)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Combines the given array of Objects into a single string using the
|
|
||||||
* seperator character and each Object's toString() method. between each
|
|
||||||
* part.
|
|
||||||
*
|
|
||||||
* @param parts
|
|
||||||
* @param seperator
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String combine(Object[] parts, char seperator) {
|
|
||||||
if (parts == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
for (int i = 0; i < parts.length; i++) {
|
|
||||||
sb.append(parts[i].toString());
|
|
||||||
if (i < parts.length - 1) {
|
|
||||||
sb.append(seperator);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String base64Decode(String encoded) {
|
|
||||||
if (encoded == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
byte[] decoded = new Base64().decode(encoded.getBytes());
|
|
||||||
return new String(decoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String base64Encode(String s) {
|
|
||||||
if (s == null) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
byte[] encoded = new Base64().encode(s.getBytes());
|
|
||||||
return new String(encoded);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean requiredFieldValid(TextView view) {
|
|
||||||
return view.getText() != null && view.getText().length() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static boolean requiredFieldValid(Editable s) {
|
|
||||||
return s != null && s.length() > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures that the given string starts and ends with the double quote character. The string is not modified in any way except to add the
|
|
||||||
* double quote character to start and end if it's not already there.
|
|
||||||
* sample -> "sample"
|
|
||||||
* "sample" -> "sample"
|
|
||||||
* ""sample"" -> "sample"
|
|
||||||
* "sample"" -> "sample"
|
|
||||||
* sa"mp"le -> "sa"mp"le"
|
|
||||||
* "sa"mp"le" -> "sa"mp"le"
|
|
||||||
* (empty string) -> ""
|
|
||||||
* " -> ""
|
|
||||||
* @param s
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String quoteString(String s) {
|
|
||||||
if (s == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!s.matches("^\".*\"$")) {
|
|
||||||
return "\"" + s + "\"";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A fast version of URLDecoder.decode() that works only with UTF-8 and does only two
|
|
||||||
* allocations. This version is around 3x as fast as the standard one and I'm using it
|
|
||||||
* hundreds of times in places that slow down the UI, so it helps.
|
|
||||||
*/
|
|
||||||
public static String fastUrlDecode(String s) {
|
|
||||||
try {
|
|
||||||
byte[] bytes = s.getBytes("UTF-8");
|
|
||||||
byte ch;
|
|
||||||
int length = 0;
|
|
||||||
for (int i = 0, count = bytes.length; i < count; i++) {
|
|
||||||
ch = bytes[i];
|
|
||||||
if (ch == '%') {
|
|
||||||
int h = (bytes[i + 1] - '0');
|
|
||||||
int l = (bytes[i + 2] - '0');
|
|
||||||
if (h > 9) {
|
|
||||||
h -= 7;
|
|
||||||
}
|
|
||||||
if (l > 9) {
|
|
||||||
l -= 7;
|
|
||||||
}
|
|
||||||
bytes[length] = (byte) ((h << 4) | l);
|
|
||||||
i += 2;
|
|
||||||
}
|
|
||||||
else if (ch == '+') {
|
|
||||||
bytes[length] = ' ';
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
bytes[length] = bytes[i];
|
|
||||||
}
|
|
||||||
length++;
|
|
||||||
}
|
|
||||||
return new String(bytes, 0, length, "UTF-8");
|
|
||||||
}
|
|
||||||
catch (UnsupportedEncodingException uee) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the specified date is within today. Returns false otherwise.
|
|
||||||
* @param date
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static boolean isDateToday(Date date) {
|
|
||||||
// TODO But Calendar is so slowwwwwww....
|
|
||||||
Date today = new Date();
|
|
||||||
if (date.getYear() == today.getYear() &&
|
|
||||||
date.getMonth() == today.getMonth() &&
|
|
||||||
date.getDate() == today.getDate()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO disabled this method globally. It is used in all the settings screens but I just
|
|
||||||
* noticed that an unrelated icon was dimmed. Android must share drawables internally.
|
|
||||||
*/
|
|
||||||
public static void setCompoundDrawablesAlpha(TextView view, int alpha) {
|
|
||||||
// Drawable[] drawables = view.getCompoundDrawables();
|
|
||||||
// for (Drawable drawable : drawables) {
|
|
||||||
// if (drawable != null) {
|
|
||||||
// drawable.setAlpha(alpha);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,296 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity;
|
|
||||||
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.app.ListActivity;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.ContextMenu;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.ContextMenu.ContextMenuInfo;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.AdapterView.AdapterContextMenuInfo;
|
|
||||||
import android.widget.AdapterView.OnItemClickListener;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.MessagingController;
|
|
||||||
import com.fsck.k9.Preferences;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
import com.fsck.k9.activity.setup.AccountSettings;
|
|
||||||
import com.fsck.k9.activity.setup.AccountSetupBasics;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
import com.fsck.k9.mail.Store;
|
|
||||||
import com.fsck.k9.mail.store.LocalStore;
|
|
||||||
import com.fsck.k9.mail.store.LocalStore.LocalFolder;
|
|
||||||
|
|
||||||
public class Accounts extends ListActivity implements OnItemClickListener, OnClickListener {
|
|
||||||
private static final int DIALOG_REMOVE_ACCOUNT = 1;
|
|
||||||
/**
|
|
||||||
* Key codes used to open a debug settings screen.
|
|
||||||
*/
|
|
||||||
private static int[] secretKeyCodes = {
|
|
||||||
KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_E, KeyEvent.KEYCODE_B, KeyEvent.KEYCODE_U,
|
|
||||||
KeyEvent.KEYCODE_G
|
|
||||||
};
|
|
||||||
|
|
||||||
private int mSecretKeyCodeIndex = 0;
|
|
||||||
private Account mSelectedContextAccount;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle icicle) {
|
|
||||||
super.onCreate(icicle);
|
|
||||||
setContentView(R.layout.accounts);
|
|
||||||
ListView listView = getListView();
|
|
||||||
listView.setOnItemClickListener(this);
|
|
||||||
listView.setItemsCanFocus(false);
|
|
||||||
listView.setEmptyView(findViewById(R.id.empty));
|
|
||||||
findViewById(R.id.add_new_account).setOnClickListener(this);
|
|
||||||
registerForContextMenu(listView);
|
|
||||||
|
|
||||||
if (icicle != null && icicle.containsKey("selectedContextAccount")) {
|
|
||||||
mSelectedContextAccount = (Account) icicle.getSerializable("selectedContextAccount");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
if (mSelectedContextAccount != null) {
|
|
||||||
outState.putSerializable("selectedContextAccount", mSelectedContextAccount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
|
|
||||||
NotificationManager notifMgr = (NotificationManager)
|
|
||||||
getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
notifMgr.cancel(1);
|
|
||||||
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void refresh() {
|
|
||||||
Account[] accounts = Preferences.getPreferences(this).getAccounts();
|
|
||||||
getListView().setAdapter(new AccountsAdapter(accounts));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onAddNewAccount() {
|
|
||||||
AccountSetupBasics.actionNewAccount(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onEditAccount(Account account) {
|
|
||||||
AccountSettings.actionSettings(this, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onRefresh() {
|
|
||||||
MessagingController.getInstance(getApplication()).checkMail(this, null, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onCompose() {
|
|
||||||
Account defaultAccount =
|
|
||||||
Preferences.getPreferences(this).getDefaultAccount();
|
|
||||||
if (defaultAccount != null) {
|
|
||||||
MessageCompose.actionCompose(this, defaultAccount);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
onAddNewAccount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onOpenAccount(Account account) {
|
|
||||||
FolderMessageList.actionHandleAccount(this, account);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(View view) {
|
|
||||||
if (view.getId() == R.id.add_new_account) {
|
|
||||||
onAddNewAccount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onDeleteAccount(Account account) {
|
|
||||||
mSelectedContextAccount = account;
|
|
||||||
showDialog(DIALOG_REMOVE_ACCOUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(int id) {
|
|
||||||
switch (id) {
|
|
||||||
case DIALOG_REMOVE_ACCOUNT:
|
|
||||||
return createRemoveAccountDialog();
|
|
||||||
}
|
|
||||||
return super.onCreateDialog(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dialog createRemoveAccountDialog() {
|
|
||||||
return new AlertDialog.Builder(this)
|
|
||||||
.setTitle(R.string.account_delete_dlg_title)
|
|
||||||
.setMessage(getString(R.string.account_delete_dlg_instructions_fmt,
|
|
||||||
mSelectedContextAccount.getDescription()))
|
|
||||||
.setPositiveButton(R.string.okay_action, new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int whichButton) {
|
|
||||||
dismissDialog(DIALOG_REMOVE_ACCOUNT);
|
|
||||||
try {
|
|
||||||
((LocalStore)Store.getInstance(
|
|
||||||
mSelectedContextAccount.getLocalStoreUri(),
|
|
||||||
getApplication())).delete();
|
|
||||||
} catch (Exception e) {
|
|
||||||
// Ignore
|
|
||||||
}
|
|
||||||
mSelectedContextAccount.delete(Preferences.getPreferences(Accounts.this));
|
|
||||||
k9.setServicesEnabled(Accounts.this);
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.cancel_action, new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int whichButton) {
|
|
||||||
dismissDialog(DIALOG_REMOVE_ACCOUNT);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.create();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean onContextItemSelected(MenuItem item) {
|
|
||||||
AdapterContextMenuInfo menuInfo = (AdapterContextMenuInfo)item.getMenuInfo();
|
|
||||||
Account account = (Account)getListView().getItemAtPosition(menuInfo.position);
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.delete_account:
|
|
||||||
onDeleteAccount(account);
|
|
||||||
break;
|
|
||||||
case R.id.edit_account:
|
|
||||||
onEditAccount(account);
|
|
||||||
break;
|
|
||||||
case R.id.open:
|
|
||||||
onOpenAccount(account);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onItemClick(AdapterView parent, View view, int position, long id) {
|
|
||||||
Account account = (Account)parent.getItemAtPosition(position);
|
|
||||||
onOpenAccount(account);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.add_new_account:
|
|
||||||
onAddNewAccount();
|
|
||||||
break;
|
|
||||||
case R.id.check_mail:
|
|
||||||
onRefresh();
|
|
||||||
break;
|
|
||||||
case R.id.compose:
|
|
||||||
onCompose();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
super.onCreateOptionsMenu(menu);
|
|
||||||
getMenuInflater().inflate(R.menu.accounts_option, menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
|
|
||||||
super.onCreateContextMenu(menu, v, menuInfo);
|
|
||||||
menu.setHeaderTitle(R.string.accounts_context_menu_title);
|
|
||||||
getMenuInflater().inflate(R.menu.accounts_context, menu);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
||||||
if (event.getKeyCode() == secretKeyCodes[mSecretKeyCodeIndex]) {
|
|
||||||
mSecretKeyCodeIndex++;
|
|
||||||
if (mSecretKeyCodeIndex == secretKeyCodes.length) {
|
|
||||||
mSecretKeyCodeIndex = 0;
|
|
||||||
startActivity(new Intent(this, Debug.class));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
mSecretKeyCodeIndex = 0;
|
|
||||||
}
|
|
||||||
return super.onKeyDown(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
class AccountsAdapter extends ArrayAdapter<Account> {
|
|
||||||
public AccountsAdapter(Account[] accounts) {
|
|
||||||
super(Accounts.this, 0, accounts);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
|
||||||
Account account = getItem(position);
|
|
||||||
View view;
|
|
||||||
if (convertView != null) {
|
|
||||||
view = convertView;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
view = getLayoutInflater().inflate(R.layout.accounts_item, parent, false);
|
|
||||||
}
|
|
||||||
AccountViewHolder holder = (AccountViewHolder) view.getTag();
|
|
||||||
if (holder == null) {
|
|
||||||
holder = new AccountViewHolder();
|
|
||||||
holder.description = (TextView) view.findViewById(R.id.description);
|
|
||||||
holder.email = (TextView) view.findViewById(R.id.email);
|
|
||||||
holder.newMessageCount = (TextView) view.findViewById(R.id.new_message_count);
|
|
||||||
view.setTag(holder);
|
|
||||||
}
|
|
||||||
holder.description.setText(account.getDescription());
|
|
||||||
holder.email.setText(account.getEmail());
|
|
||||||
if (account.getEmail().equals(account.getDescription())) {
|
|
||||||
holder.email.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
int unreadMessageCount = 0;
|
|
||||||
try {
|
|
||||||
LocalStore localStore = (LocalStore) Store.getInstance(
|
|
||||||
account.getLocalStoreUri(),
|
|
||||||
getApplication());
|
|
||||||
LocalFolder localFolder = (LocalFolder) localStore.getFolder(k9.INBOX);
|
|
||||||
if (localFolder.exists()) {
|
|
||||||
unreadMessageCount = localFolder.getUnreadMessageCount();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (MessagingException me) {
|
|
||||||
/*
|
|
||||||
* This is not expected to fail under normal circumstances.
|
|
||||||
*/
|
|
||||||
throw new RuntimeException("Unable to get unread count from local store.", me);
|
|
||||||
}
|
|
||||||
holder.newMessageCount.setText(Integer.toString(unreadMessageCount));
|
|
||||||
holder.newMessageCount.setVisibility(unreadMessageCount > 0 ? View.VISIBLE : View.GONE);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AccountViewHolder {
|
|
||||||
public TextView description;
|
|
||||||
public TextView email;
|
|
||||||
public TextView newMessageCount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -1,73 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.CompoundButton;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
|
||||||
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.Preferences;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
|
|
||||||
public class Debug extends Activity implements OnCheckedChangeListener {
|
|
||||||
private TextView mVersionView;
|
|
||||||
private CheckBox mEnableDebugLoggingView;
|
|
||||||
private CheckBox mEnableSensitiveLoggingView;
|
|
||||||
|
|
||||||
private Preferences mPreferences;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
setContentView(R.layout.debug);
|
|
||||||
|
|
||||||
mPreferences = Preferences.getPreferences(this);
|
|
||||||
|
|
||||||
mVersionView = (TextView)findViewById(R.id.version);
|
|
||||||
mEnableDebugLoggingView = (CheckBox)findViewById(R.id.debug_logging);
|
|
||||||
mEnableSensitiveLoggingView = (CheckBox)findViewById(R.id.sensitive_logging);
|
|
||||||
|
|
||||||
mEnableDebugLoggingView.setOnCheckedChangeListener(this);
|
|
||||||
mEnableSensitiveLoggingView.setOnCheckedChangeListener(this);
|
|
||||||
|
|
||||||
mVersionView.setText(String.format(getString(R.string.debug_version_fmt).toString(),
|
|
||||||
getString(R.string.build_number)));
|
|
||||||
|
|
||||||
mEnableDebugLoggingView.setChecked(k9.DEBUG);
|
|
||||||
mEnableSensitiveLoggingView.setChecked(k9.DEBUG_SENSITIVE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
|
||||||
if (buttonView.getId() == R.id.debug_logging) {
|
|
||||||
k9.DEBUG = isChecked;
|
|
||||||
mPreferences.setEnableDebugLogging(k9.DEBUG);
|
|
||||||
} else if (buttonView.getId() == R.id.sensitive_logging) {
|
|
||||||
k9.DEBUG_SENSITIVE = isChecked;
|
|
||||||
mPreferences.setEnableSensitiveLogging(k9.DEBUG_SENSITIVE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
int id = item.getItemId();
|
|
||||||
if (id == R.id.dump_settings) {
|
|
||||||
Preferences.getPreferences(this).dump();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
super.onCreateOptionsMenu(menu);
|
|
||||||
getMenuInflater().inflate(R.menu.debug_option, menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,916 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.media.MediaScannerConnection;
|
|
||||||
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Environment;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Process;
|
|
||||||
import android.text.Spannable;
|
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.util.Regex;
|
|
||||||
import android.text.util.Linkify;
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.Window;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.webkit.CacheManager;
|
|
||||||
import android.webkit.UrlInterceptHandler;
|
|
||||||
import android.webkit.WebView;
|
|
||||||
import android.webkit.CacheManager.CacheResult;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.MessagingController;
|
|
||||||
import com.fsck.k9.MessagingListener;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
import com.fsck.k9.Utility;
|
|
||||||
import com.fsck.k9.mail.Address;
|
|
||||||
import com.fsck.k9.mail.Message;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
import com.fsck.k9.mail.Multipart;
|
|
||||||
import com.fsck.k9.mail.Part;
|
|
||||||
import com.fsck.k9.mail.Message.RecipientType;
|
|
||||||
import com.fsck.k9.mail.internet.MimeHeader;
|
|
||||||
import com.fsck.k9.mail.internet.MimeUtility;
|
|
||||||
import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBody;
|
|
||||||
import com.fsck.k9.mail.store.LocalStore.LocalAttachmentBodyPart;
|
|
||||||
import com.fsck.k9.mail.store.LocalStore.LocalMessage;
|
|
||||||
import com.fsck.k9.provider.AttachmentProvider;
|
|
||||||
|
|
||||||
public class MessageView extends Activity
|
|
||||||
implements UrlInterceptHandler, OnClickListener {
|
|
||||||
private static final String EXTRA_ACCOUNT = "com.fsck.k9.MessageView_account";
|
|
||||||
private static final String EXTRA_FOLDER = "com.fsck.k9.MessageView_folder";
|
|
||||||
private static final String EXTRA_MESSAGE = "com.fsck.k9.MessageView_message";
|
|
||||||
private static final String EXTRA_FOLDER_UIDS = "com.fsck.k9.MessageView_folderUids";
|
|
||||||
private static final String EXTRA_NEXT = "com.fsck.k9.MessageView_next";
|
|
||||||
|
|
||||||
private TextView mFromView;
|
|
||||||
private TextView mDateView;
|
|
||||||
private TextView mToView;
|
|
||||||
private TextView mSubjectView;
|
|
||||||
private WebView mMessageContentView;
|
|
||||||
private LinearLayout mAttachments;
|
|
||||||
private View mAttachmentIcon;
|
|
||||||
private View mShowPicturesSection;
|
|
||||||
|
|
||||||
private Account mAccount;
|
|
||||||
private String mFolder;
|
|
||||||
private String mMessageUid;
|
|
||||||
private ArrayList<String> mFolderUids;
|
|
||||||
|
|
||||||
private Message mMessage;
|
|
||||||
private String mNextMessageUid = null;
|
|
||||||
private String mPreviousMessageUid = null;
|
|
||||||
|
|
||||||
private DateFormat mDateTimeFormat = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT);
|
|
||||||
private DateFormat mTimeFormat = DateFormat.getTimeInstance(DateFormat.SHORT);
|
|
||||||
|
|
||||||
private Listener mListener = new Listener();
|
|
||||||
private MessageViewHandler mHandler = new MessageViewHandler();
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
||||||
switch (keyCode) {
|
|
||||||
case KeyEvent.KEYCODE_DEL: { onDelete(); return true;}
|
|
||||||
case KeyEvent.KEYCODE_D: { onDelete(); return true;}
|
|
||||||
case KeyEvent.KEYCODE_F: { onForward(); return true;}
|
|
||||||
case KeyEvent.KEYCODE_A: { onReplyAll(); return true; }
|
|
||||||
case KeyEvent.KEYCODE_R: { onReply(); return true; }
|
|
||||||
case KeyEvent.KEYCODE_J: { onPrevious(); return true; }
|
|
||||||
case KeyEvent.KEYCODE_K: { onNext(); return true; }
|
|
||||||
case KeyEvent.KEYCODE_Z: { if (event.isShiftPressed()) {
|
|
||||||
mMessageContentView.zoomIn();
|
|
||||||
} else {
|
|
||||||
mMessageContentView.zoomOut();
|
|
||||||
}
|
|
||||||
return true; }
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
return super.onKeyDown(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class MessageViewHandler extends Handler {
|
|
||||||
private static final int MSG_PROGRESS = 2;
|
|
||||||
private static final int MSG_ADD_ATTACHMENT = 3;
|
|
||||||
private static final int MSG_SET_ATTACHMENTS_ENABLED = 4;
|
|
||||||
private static final int MSG_SET_HEADERS = 5;
|
|
||||||
private static final int MSG_NETWORK_ERROR = 6;
|
|
||||||
private static final int MSG_ATTACHMENT_SAVED = 7;
|
|
||||||
private static final int MSG_ATTACHMENT_NOT_SAVED = 8;
|
|
||||||
private static final int MSG_SHOW_SHOW_PICTURES = 9;
|
|
||||||
private static final int MSG_FETCHING_ATTACHMENT = 10;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void handleMessage(android.os.Message msg) {
|
|
||||||
switch (msg.what) {
|
|
||||||
case MSG_PROGRESS:
|
|
||||||
setProgressBarIndeterminateVisibility(msg.arg1 != 0);
|
|
||||||
break;
|
|
||||||
case MSG_ADD_ATTACHMENT:
|
|
||||||
mAttachments.addView((View) msg.obj);
|
|
||||||
mAttachments.setVisibility(View.VISIBLE);
|
|
||||||
break;
|
|
||||||
case MSG_SET_ATTACHMENTS_ENABLED:
|
|
||||||
for (int i = 0, count = mAttachments.getChildCount(); i < count; i++) {
|
|
||||||
Attachment attachment = (Attachment) mAttachments.getChildAt(i).getTag();
|
|
||||||
attachment.viewButton.setEnabled(msg.arg1 == 1);
|
|
||||||
attachment.downloadButton.setEnabled(msg.arg1 == 1);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case MSG_SET_HEADERS:
|
|
||||||
String[] values = (String[]) msg.obj;
|
|
||||||
setTitle(values[0]);
|
|
||||||
mSubjectView.setText(values[0]);
|
|
||||||
mFromView.setText(values[1]);
|
|
||||||
mDateView.setText(values[2]);
|
|
||||||
mToView.setText(values[3]);
|
|
||||||
mAttachmentIcon.setVisibility(msg.arg1 == 1 ? View.VISIBLE : View.GONE);
|
|
||||||
break;
|
|
||||||
case MSG_NETWORK_ERROR:
|
|
||||||
Toast.makeText(MessageView.this,
|
|
||||||
R.string.status_network_error, Toast.LENGTH_LONG).show();
|
|
||||||
break;
|
|
||||||
case MSG_ATTACHMENT_SAVED:
|
|
||||||
Toast.makeText(MessageView.this, String.format(
|
|
||||||
getString(R.string.message_view_status_attachment_saved), msg.obj),
|
|
||||||
Toast.LENGTH_LONG).show();
|
|
||||||
break;
|
|
||||||
case MSG_ATTACHMENT_NOT_SAVED:
|
|
||||||
Toast.makeText(MessageView.this,
|
|
||||||
getString(R.string.message_view_status_attachment_not_saved),
|
|
||||||
Toast.LENGTH_LONG).show();
|
|
||||||
break;
|
|
||||||
case MSG_SHOW_SHOW_PICTURES:
|
|
||||||
mShowPicturesSection.setVisibility(msg.arg1 == 1 ? View.VISIBLE : View.GONE);
|
|
||||||
break;
|
|
||||||
case MSG_FETCHING_ATTACHMENT:
|
|
||||||
Toast.makeText(MessageView.this,
|
|
||||||
getString(R.string.message_view_fetching_attachment_toast),
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
super.handleMessage(msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void progress(boolean progress) {
|
|
||||||
android.os.Message msg = new android.os.Message();
|
|
||||||
msg.what = MSG_PROGRESS;
|
|
||||||
msg.arg1 = progress ? 1 : 0;
|
|
||||||
sendMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addAttachment(View attachmentView) {
|
|
||||||
android.os.Message msg = new android.os.Message();
|
|
||||||
msg.what = MSG_ADD_ATTACHMENT;
|
|
||||||
msg.obj = attachmentView;
|
|
||||||
sendMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAttachmentsEnabled(boolean enabled) {
|
|
||||||
android.os.Message msg = new android.os.Message();
|
|
||||||
msg.what = MSG_SET_ATTACHMENTS_ENABLED;
|
|
||||||
msg.arg1 = enabled ? 1 : 0;
|
|
||||||
sendMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHeaders(
|
|
||||||
String subject,
|
|
||||||
String from,
|
|
||||||
String date,
|
|
||||||
String to,
|
|
||||||
boolean hasAttachments) {
|
|
||||||
android.os.Message msg = new android.os.Message();
|
|
||||||
msg.what = MSG_SET_HEADERS;
|
|
||||||
msg.arg1 = hasAttachments ? 1 : 0;
|
|
||||||
msg.obj = new String[] { subject, from, date, to };
|
|
||||||
sendMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void networkError() {
|
|
||||||
sendEmptyMessage(MSG_NETWORK_ERROR);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void attachmentSaved(String filename) {
|
|
||||||
android.os.Message msg = new android.os.Message();
|
|
||||||
msg.what = MSG_ATTACHMENT_SAVED;
|
|
||||||
msg.obj = filename;
|
|
||||||
sendMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void attachmentNotSaved() {
|
|
||||||
sendEmptyMessage(MSG_ATTACHMENT_NOT_SAVED);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void fetchingAttachment() {
|
|
||||||
sendEmptyMessage(MSG_FETCHING_ATTACHMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void showShowPictures(boolean show) {
|
|
||||||
android.os.Message msg = new android.os.Message();
|
|
||||||
msg.what = MSG_SHOW_SHOW_PICTURES;
|
|
||||||
msg.arg1 = show ? 1 : 0;
|
|
||||||
sendMessage(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class Attachment {
|
|
||||||
public String name;
|
|
||||||
public String contentType;
|
|
||||||
public long size;
|
|
||||||
public LocalAttachmentBodyPart part;
|
|
||||||
public Button viewButton;
|
|
||||||
public Button downloadButton;
|
|
||||||
public ImageView iconView;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void actionView(Context context, Account account,
|
|
||||||
String folder, String messageUid, ArrayList<String> folderUids) {
|
|
||||||
actionView(context, account, folder, messageUid, folderUids, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void actionView(Context context, Account account,
|
|
||||||
String folder, String messageUid, ArrayList<String> folderUids, Bundle extras) {
|
|
||||||
Intent i = new Intent(context, MessageView.class);
|
|
||||||
i.putExtra(EXTRA_ACCOUNT, account);
|
|
||||||
i.putExtra(EXTRA_FOLDER, folder);
|
|
||||||
i.putExtra(EXTRA_MESSAGE, messageUid);
|
|
||||||
i.putExtra(EXTRA_FOLDER_UIDS, folderUids);
|
|
||||||
if (extras != null) {
|
|
||||||
i.putExtras(extras);
|
|
||||||
}
|
|
||||||
context.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCreate(Bundle icicle) {
|
|
||||||
super.onCreate(icicle);
|
|
||||||
|
|
||||||
requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
|
|
||||||
|
|
||||||
setContentView(R.layout.message_view);
|
|
||||||
|
|
||||||
mFromView = (TextView)findViewById(R.id.from);
|
|
||||||
mToView = (TextView)findViewById(R.id.to);
|
|
||||||
mSubjectView = (TextView)findViewById(R.id.subject);
|
|
||||||
mDateView = (TextView)findViewById(R.id.date);
|
|
||||||
mMessageContentView = (WebView)findViewById(R.id.message_content);
|
|
||||||
mAttachments = (LinearLayout)findViewById(R.id.attachments);
|
|
||||||
mAttachmentIcon = findViewById(R.id.attachment);
|
|
||||||
mShowPicturesSection = findViewById(R.id.show_pictures_section);
|
|
||||||
|
|
||||||
mMessageContentView.setVerticalScrollBarEnabled(false);
|
|
||||||
mAttachments.setVisibility(View.GONE);
|
|
||||||
mAttachmentIcon.setVisibility(View.GONE);
|
|
||||||
|
|
||||||
findViewById(R.id.reply).setOnClickListener(this);
|
|
||||||
findViewById(R.id.reply_all).setOnClickListener(this);
|
|
||||||
findViewById(R.id.delete).setOnClickListener(this);
|
|
||||||
findViewById(R.id.show_pictures).setOnClickListener(this);
|
|
||||||
|
|
||||||
// UrlInterceptRegistry.registerHandler(this);
|
|
||||||
|
|
||||||
mMessageContentView.getSettings().setBlockNetworkImage(true);
|
|
||||||
mMessageContentView.getSettings().setSupportZoom(true);
|
|
||||||
|
|
||||||
setTitle("");
|
|
||||||
|
|
||||||
Intent intent = getIntent();
|
|
||||||
mAccount = (Account) intent.getSerializableExtra(EXTRA_ACCOUNT);
|
|
||||||
mFolder = intent.getStringExtra(EXTRA_FOLDER);
|
|
||||||
mMessageUid = intent.getStringExtra(EXTRA_MESSAGE);
|
|
||||||
mFolderUids = intent.getStringArrayListExtra(EXTRA_FOLDER_UIDS);
|
|
||||||
|
|
||||||
View next = findViewById(R.id.next);
|
|
||||||
View previous = findViewById(R.id.previous);
|
|
||||||
/*
|
|
||||||
* Next and Previous Message are not shown in landscape mode, so
|
|
||||||
* we need to check before we use them.
|
|
||||||
*/
|
|
||||||
if (next != null && previous != null) {
|
|
||||||
next.setOnClickListener(this);
|
|
||||||
previous.setOnClickListener(this);
|
|
||||||
|
|
||||||
findSurroundingMessagesUid();
|
|
||||||
|
|
||||||
previous.setVisibility(mPreviousMessageUid != null ? View.VISIBLE : View.GONE);
|
|
||||||
next.setVisibility(mNextMessageUid != null ? View.VISIBLE : View.GONE);
|
|
||||||
|
|
||||||
boolean goNext = intent.getBooleanExtra(EXTRA_NEXT, false);
|
|
||||||
if (goNext) {
|
|
||||||
next.requestFocus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MessagingController.getInstance(getApplication()).addListener(mListener);
|
|
||||||
new Thread() {
|
|
||||||
public void run() {
|
|
||||||
// TODO this is a spot that should be eventually handled by a MessagingController
|
|
||||||
// thread pool. We want it in a thread but it can't be blocked by the normal
|
|
||||||
// synchronization stuff in MC.
|
|
||||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
|
||||||
MessagingController.getInstance(getApplication()).loadMessageForView(
|
|
||||||
mAccount,
|
|
||||||
mFolder,
|
|
||||||
mMessageUid,
|
|
||||||
mListener);
|
|
||||||
}
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void findSurroundingMessagesUid() {
|
|
||||||
for (int i = 0, count = mFolderUids.size(); i < count; i++) {
|
|
||||||
String messageUid = mFolderUids.get(i);
|
|
||||||
if (messageUid.equals(mMessageUid)) {
|
|
||||||
if (i != 0) {
|
|
||||||
mPreviousMessageUid = mFolderUids.get(i - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (i != count - 1) {
|
|
||||||
mNextMessageUid = mFolderUids.get(i + 1);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
MessagingController.getInstance(getApplication()).addListener(mListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPause() {
|
|
||||||
super.onPause();
|
|
||||||
MessagingController.getInstance(getApplication()).removeListener(mListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onDelete() {
|
|
||||||
if (mMessage != null) {
|
|
||||||
MessagingController.getInstance(getApplication()).deleteMessage(
|
|
||||||
mAccount,
|
|
||||||
mFolder,
|
|
||||||
mMessage,
|
|
||||||
null);
|
|
||||||
Toast.makeText(this, R.string.message_deleted_toast, Toast.LENGTH_SHORT).show();
|
|
||||||
|
|
||||||
// Remove this message's Uid locally
|
|
||||||
mFolderUids.remove(mMessage.getUid());
|
|
||||||
// Check if we have previous/next messages available before choosing
|
|
||||||
// which one to display
|
|
||||||
findSurroundingMessagesUid();
|
|
||||||
|
|
||||||
if (mPreviousMessageUid != null) {
|
|
||||||
onPrevious();
|
|
||||||
} else if (mNextMessageUid != null) {
|
|
||||||
onNext();
|
|
||||||
} else {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onReply() {
|
|
||||||
if (mMessage != null) {
|
|
||||||
MessageCompose.actionReply(this, mAccount, mMessage, false);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onReplyAll() {
|
|
||||||
if (mMessage != null) {
|
|
||||||
MessageCompose.actionReply(this, mAccount, mMessage, true);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onForward() {
|
|
||||||
if (mMessage != null) {
|
|
||||||
MessageCompose.actionForward(this, mAccount, mMessage);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onNext() {
|
|
||||||
Bundle extras = new Bundle(1);
|
|
||||||
extras.putBoolean(EXTRA_NEXT, true);
|
|
||||||
MessageView.actionView(this, mAccount, mFolder, mNextMessageUid, mFolderUids, extras);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onPrevious() {
|
|
||||||
MessageView.actionView(this, mAccount, mFolder, mPreviousMessageUid, mFolderUids);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onMarkAsUnread() {
|
|
||||||
MessagingController.getInstance(getApplication()).markMessageRead(
|
|
||||||
mAccount,
|
|
||||||
mFolder,
|
|
||||||
mMessage.getUid(),
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a unique file in the given directory by appending a hyphen
|
|
||||||
* and a number to the given filename.
|
|
||||||
* @param directory
|
|
||||||
* @param filename
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private File createUniqueFile(File directory, String filename) {
|
|
||||||
File file = new File(directory, filename);
|
|
||||||
if (!file.exists()) {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
// Get the extension of the file, if any.
|
|
||||||
int index = filename.lastIndexOf('.');
|
|
||||||
String format;
|
|
||||||
if (index != -1) {
|
|
||||||
String name = filename.substring(0, index);
|
|
||||||
String extension = filename.substring(index);
|
|
||||||
format = name + "-%d" + extension;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
format = filename + "-%d";
|
|
||||||
}
|
|
||||||
for (int i = 2; i < Integer.MAX_VALUE; i++) {
|
|
||||||
file = new File(directory, String.format(format, i));
|
|
||||||
if (!file.exists()) {
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onDownloadAttachment(Attachment attachment) {
|
|
||||||
if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
|
|
||||||
/*
|
|
||||||
* Abort early if there's no place to save the attachment. We don't want to spend
|
|
||||||
* the time downloading it and then abort.
|
|
||||||
*/
|
|
||||||
Toast.makeText(this,
|
|
||||||
getString(R.string.message_view_status_attachment_not_saved),
|
|
||||||
Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
MessagingController.getInstance(getApplication()).loadAttachment(
|
|
||||||
mAccount,
|
|
||||||
mMessage,
|
|
||||||
attachment.part,
|
|
||||||
new Object[] { true, attachment },
|
|
||||||
mListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onViewAttachment(Attachment attachment) {
|
|
||||||
MessagingController.getInstance(getApplication()).loadAttachment(
|
|
||||||
mAccount,
|
|
||||||
mMessage,
|
|
||||||
attachment.part,
|
|
||||||
new Object[] { false, attachment },
|
|
||||||
mListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onShowPictures() {
|
|
||||||
mMessageContentView.getSettings().setBlockNetworkImage(false);
|
|
||||||
mShowPicturesSection.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(View view) {
|
|
||||||
switch (view.getId()) {
|
|
||||||
case R.id.reply:
|
|
||||||
onReply();
|
|
||||||
break;
|
|
||||||
case R.id.reply_all:
|
|
||||||
onReplyAll();
|
|
||||||
break;
|
|
||||||
case R.id.delete:
|
|
||||||
onDelete();
|
|
||||||
break;
|
|
||||||
case R.id.next:
|
|
||||||
onNext();
|
|
||||||
break;
|
|
||||||
case R.id.previous:
|
|
||||||
onPrevious();
|
|
||||||
break;
|
|
||||||
case R.id.download:
|
|
||||||
onDownloadAttachment((Attachment) view.getTag());
|
|
||||||
break;
|
|
||||||
case R.id.view:
|
|
||||||
onViewAttachment((Attachment) view.getTag());
|
|
||||||
break;
|
|
||||||
case R.id.show_pictures:
|
|
||||||
onShowPictures();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.delete:
|
|
||||||
onDelete();
|
|
||||||
break;
|
|
||||||
case R.id.reply:
|
|
||||||
onReply();
|
|
||||||
break;
|
|
||||||
case R.id.reply_all:
|
|
||||||
onReplyAll();
|
|
||||||
break;
|
|
||||||
case R.id.forward:
|
|
||||||
onForward();
|
|
||||||
break;
|
|
||||||
case R.id.mark_as_unread:
|
|
||||||
onMarkAsUnread();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
|
||||||
super.onCreateOptionsMenu(menu);
|
|
||||||
getMenuInflater().inflate(R.menu.message_view_option, menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public CacheResult service(String url, Map<String, String> headers) {
|
|
||||||
String prefix = "http://cid/";
|
|
||||||
if (url.startsWith(prefix)) {
|
|
||||||
try {
|
|
||||||
String contentId = url.substring(prefix.length());
|
|
||||||
final Part part = MimeUtility.findPartByContentId(mMessage, "<" + contentId + ">");
|
|
||||||
if (part != null) {
|
|
||||||
CacheResult cr = new CacheManager.CacheResult();
|
|
||||||
// TODO looks fixed in Mainline, cr.setInputStream
|
|
||||||
// part.getBody().writeTo(cr.getStream());
|
|
||||||
return cr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
// TODO
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap getPreviewIcon(Attachment attachment) throws MessagingException {
|
|
||||||
try {
|
|
||||||
return BitmapFactory.decodeStream(
|
|
||||||
getContentResolver().openInputStream(
|
|
||||||
AttachmentProvider.getAttachmentThumbnailUri(mAccount,
|
|
||||||
attachment.part.getAttachmentId(),
|
|
||||||
62,
|
|
||||||
62)));
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
/*
|
|
||||||
* We don't care what happened, we just return null for the preview icon.
|
|
||||||
*/
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Formats the given size as a String in bytes, kB, MB or GB with a single digit
|
|
||||||
* of precision. Ex: 12,315,000 = 12.3 MB
|
|
||||||
*/
|
|
||||||
public static String formatSize(float size) {
|
|
||||||
long kb = 1024;
|
|
||||||
long mb = (kb * 1024);
|
|
||||||
long gb = (mb * 1024);
|
|
||||||
if (size < kb) {
|
|
||||||
return String.format("%d bytes", (int) size);
|
|
||||||
}
|
|
||||||
else if (size < mb) {
|
|
||||||
return String.format("%.1f kB", size / kb);
|
|
||||||
}
|
|
||||||
else if (size < gb) {
|
|
||||||
return String.format("%.1f MB", size / mb);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return String.format("%.1f GB", size / gb);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void renderAttachments(Part part, int depth) throws MessagingException {
|
|
||||||
String contentType = MimeUtility.unfoldAndDecode(part.getContentType());
|
|
||||||
String name = MimeUtility.getHeaderParameter(contentType, "name");
|
|
||||||
if (name != null) {
|
|
||||||
/*
|
|
||||||
* We're guaranteed size because LocalStore.fetch puts it there.
|
|
||||||
*/
|
|
||||||
String contentDisposition = MimeUtility.unfoldAndDecode(part.getDisposition());
|
|
||||||
int size = Integer.parseInt(MimeUtility.getHeaderParameter(contentDisposition, "size"));
|
|
||||||
|
|
||||||
Attachment attachment = new Attachment();
|
|
||||||
attachment.size = size;
|
|
||||||
attachment.contentType = part.getMimeType();
|
|
||||||
attachment.name = name;
|
|
||||||
attachment.part = (LocalAttachmentBodyPart) part;
|
|
||||||
|
|
||||||
LayoutInflater inflater = getLayoutInflater();
|
|
||||||
View view = inflater.inflate(R.layout.message_view_attachment, null);
|
|
||||||
|
|
||||||
TextView attachmentName = (TextView)view.findViewById(R.id.attachment_name);
|
|
||||||
TextView attachmentInfo = (TextView)view.findViewById(R.id.attachment_info);
|
|
||||||
ImageView attachmentIcon = (ImageView)view.findViewById(R.id.attachment_icon);
|
|
||||||
Button attachmentView = (Button)view.findViewById(R.id.view);
|
|
||||||
Button attachmentDownload = (Button)view.findViewById(R.id.download);
|
|
||||||
|
|
||||||
if ((!MimeUtility.mimeTypeMatches(attachment.contentType,
|
|
||||||
k9.ACCEPTABLE_ATTACHMENT_VIEW_TYPES))
|
|
||||||
|| (MimeUtility.mimeTypeMatches(attachment.contentType,
|
|
||||||
k9.UNACCEPTABLE_ATTACHMENT_VIEW_TYPES))) {
|
|
||||||
attachmentView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
if ((!MimeUtility.mimeTypeMatches(attachment.contentType,
|
|
||||||
k9.ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))
|
|
||||||
|| (MimeUtility.mimeTypeMatches(attachment.contentType,
|
|
||||||
k9.UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES))) {
|
|
||||||
attachmentDownload.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (attachment.size > k9.MAX_ATTACHMENT_DOWNLOAD_SIZE) {
|
|
||||||
attachmentView.setVisibility(View.GONE);
|
|
||||||
attachmentDownload.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
attachment.viewButton = attachmentView;
|
|
||||||
attachment.downloadButton = attachmentDownload;
|
|
||||||
attachment.iconView = attachmentIcon;
|
|
||||||
|
|
||||||
view.setTag(attachment);
|
|
||||||
attachmentView.setOnClickListener(this);
|
|
||||||
attachmentView.setTag(attachment);
|
|
||||||
attachmentDownload.setOnClickListener(this);
|
|
||||||
attachmentDownload.setTag(attachment);
|
|
||||||
|
|
||||||
attachmentName.setText(name);
|
|
||||||
attachmentInfo.setText(formatSize(size));
|
|
||||||
|
|
||||||
Bitmap previewIcon = getPreviewIcon(attachment);
|
|
||||||
if (previewIcon != null) {
|
|
||||||
attachmentIcon.setImageBitmap(previewIcon);
|
|
||||||
}
|
|
||||||
|
|
||||||
mHandler.addAttachment(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (part.getBody() instanceof Multipart) {
|
|
||||||
Multipart mp = (Multipart)part.getBody();
|
|
||||||
for (int i = 0; i < mp.getCount(); i++) {
|
|
||||||
renderAttachments(mp.getBodyPart(i), depth + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Listener extends MessagingListener {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadMessageForViewHeadersAvailable(Account account, String folder, String uid,
|
|
||||||
final Message message) {
|
|
||||||
MessageView.this.mMessage = message;
|
|
||||||
try {
|
|
||||||
String subjectText = message.getSubject();
|
|
||||||
String fromText = Address.toFriendly(message.getFrom());
|
|
||||||
String dateText = Utility.isDateToday(message.getSentDate()) ?
|
|
||||||
mTimeFormat.format(message.getSentDate()) :
|
|
||||||
mDateTimeFormat.format(message.getSentDate());
|
|
||||||
String toText = Address.toFriendly(message.getRecipients(RecipientType.TO));
|
|
||||||
boolean hasAttachments = ((LocalMessage) message).getAttachmentCount() > 0;
|
|
||||||
mHandler.setHeaders(subjectText,
|
|
||||||
fromText,
|
|
||||||
dateText,
|
|
||||||
toText,
|
|
||||||
hasAttachments);
|
|
||||||
}
|
|
||||||
catch (MessagingException me) {
|
|
||||||
if (Config.LOGV) {
|
|
||||||
Log.v(k9.LOG_TAG, "loadMessageForViewHeadersAvailable", me);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadMessageForViewBodyAvailable(Account account, String folder, String uid,
|
|
||||||
Message message) {
|
|
||||||
SpannableString markup;
|
|
||||||
MessageView.this.mMessage = message;
|
|
||||||
try {
|
|
||||||
Part part = MimeUtility.findFirstPartByMimeType(mMessage, "text/html");
|
|
||||||
if (part == null) {
|
|
||||||
part = MimeUtility.findFirstPartByMimeType(mMessage, "text/plain");
|
|
||||||
}
|
|
||||||
if (part != null) {
|
|
||||||
String text = MimeUtility.getTextFromPart(part);
|
|
||||||
if (part.getMimeType().equalsIgnoreCase("text/html")) {
|
|
||||||
text = text.replaceAll("cid:", "http://cid/");
|
|
||||||
} else {
|
|
||||||
Matcher m = Regex.WEB_URL_PATTERN.matcher(text);
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
while (m.find()) {
|
|
||||||
int start = m.start();
|
|
||||||
if (start != 0 && text.charAt(start - 1) != '@') {
|
|
||||||
m.appendReplacement(sb, "<a href=\"$0\">$0</a>");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
m.appendReplacement(sb, "$0");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
m.appendTail(sb);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Convert plain text to HTML by replacing
|
|
||||||
* \r?\n with <br> and adding a html/body wrapper.
|
|
||||||
*/
|
|
||||||
text = sb.toString().replaceAll("\r?\n", "<br>");
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
text = "<html><body>" + text + "</body></html>";
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO this should be smarter, change to regex for img, but consider how to
|
|
||||||
* get backgroung images and a million other things that HTML allows.
|
|
||||||
*/
|
|
||||||
if (text.contains("<img")) {
|
|
||||||
mHandler.showShowPictures(true);
|
|
||||||
}
|
|
||||||
markup = new SpannableString(text);
|
|
||||||
Linkify.addLinks(markup, Linkify.ALL);
|
|
||||||
mMessageContentView.loadDataWithBaseURL("email://", markup.toString(), "text/html",
|
|
||||||
"utf-8", null);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
mMessageContentView.loadUrl("file:///android_asset/empty.html");
|
|
||||||
}
|
|
||||||
renderAttachments(mMessage, 0);
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
if (Config.LOGV) {
|
|
||||||
Log.v(k9.LOG_TAG, "loadMessageForViewBodyAvailable", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadMessageForViewFailed(Account account, String folder, String uid,
|
|
||||||
final String message) {
|
|
||||||
mHandler.post(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
setProgressBarIndeterminateVisibility(false);
|
|
||||||
mHandler.networkError();
|
|
||||||
mMessageContentView.loadUrl("file:///android_asset/empty.html");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadMessageForViewFinished(Account account, String folder, String uid,
|
|
||||||
Message message) {
|
|
||||||
mHandler.post(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
setProgressBarIndeterminateVisibility(false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadMessageForViewStarted(Account account, String folder, String uid) {
|
|
||||||
mHandler.post(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
mMessageContentView.loadUrl("file:///android_asset/loading.html");
|
|
||||||
setProgressBarIndeterminateVisibility(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadAttachmentStarted(Account account, Message message,
|
|
||||||
Part part, Object tag, boolean requiresDownload) {
|
|
||||||
mHandler.setAttachmentsEnabled(false);
|
|
||||||
mHandler.progress(true);
|
|
||||||
if (requiresDownload) {
|
|
||||||
mHandler.fetchingAttachment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadAttachmentFinished(Account account, Message message,
|
|
||||||
Part part, Object tag) {
|
|
||||||
mHandler.setAttachmentsEnabled(true);
|
|
||||||
mHandler.progress(false);
|
|
||||||
|
|
||||||
Object[] params = (Object[]) tag;
|
|
||||||
boolean download = (Boolean) params[0];
|
|
||||||
Attachment attachment = (Attachment) params[1];
|
|
||||||
|
|
||||||
if (download) {
|
|
||||||
try {
|
|
||||||
File file = createUniqueFile(Environment.getExternalStorageDirectory(),
|
|
||||||
attachment.name);
|
|
||||||
Uri uri = AttachmentProvider.getAttachmentUri(
|
|
||||||
mAccount,
|
|
||||||
attachment.part.getAttachmentId());
|
|
||||||
InputStream in = getContentResolver().openInputStream(uri);
|
|
||||||
OutputStream out = new FileOutputStream(file);
|
|
||||||
IOUtils.copy(in, out);
|
|
||||||
out.flush();
|
|
||||||
out.close();
|
|
||||||
in.close();
|
|
||||||
mHandler.attachmentSaved(file.getName());
|
|
||||||
new MediaScannerNotifier(MessageView.this, file);
|
|
||||||
}
|
|
||||||
catch (IOException ioe) {
|
|
||||||
mHandler.attachmentNotSaved();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Uri uri = AttachmentProvider.getAttachmentUri(
|
|
||||||
mAccount,
|
|
||||||
attachment.part.getAttachmentId());
|
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
||||||
intent.setData(uri);
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void loadAttachmentFailed(Account account, Message message, Part part,
|
|
||||||
Object tag, String reason) {
|
|
||||||
mHandler.setAttachmentsEnabled(true);
|
|
||||||
mHandler.progress(false);
|
|
||||||
mHandler.networkError();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class MediaScannerNotifier implements MediaScannerConnectionClient {
|
|
||||||
private MediaScannerConnection mConnection;
|
|
||||||
private File mFile;
|
|
||||||
|
|
||||||
public MediaScannerNotifier(Context context, File file) {
|
|
||||||
mFile = file;
|
|
||||||
mConnection = new MediaScannerConnection(context, this);
|
|
||||||
mConnection.connect();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onMediaScannerConnected() {
|
|
||||||
mConnection.scanFile(mFile.getAbsolutePath(), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onScanCompleted(String path, Uri uri) {
|
|
||||||
try {
|
|
||||||
if (uri != null) {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
|
||||||
intent.setData(uri);
|
|
||||||
startActivity(intent);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
mConnection.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A listener that the user can register for global, persistent progress events.
|
|
||||||
*/
|
|
||||||
public interface ProgressListener {
|
|
||||||
/**
|
|
||||||
* @param context
|
|
||||||
* @param title
|
|
||||||
* @param message
|
|
||||||
* @param currentProgress
|
|
||||||
* @param maxProgress
|
|
||||||
* @param indeterminate
|
|
||||||
*/
|
|
||||||
void showProgress(Context context, String title, String message, long currentProgress,
|
|
||||||
long maxProgress, boolean indeterminate);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context
|
|
||||||
* @param title
|
|
||||||
* @param message
|
|
||||||
* @param currentProgress
|
|
||||||
* @param maxProgress
|
|
||||||
* @param indeterminate
|
|
||||||
*/
|
|
||||||
void updateProgress(Context context, String title, String message, long currentProgress,
|
|
||||||
long maxProgress, boolean indeterminate);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param context
|
|
||||||
*/
|
|
||||||
void hideProgress(Context context);
|
|
||||||
}
|
|
@ -1,36 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.Preferences;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The Welcome activity initializes the application and decides what Activity
|
|
||||||
* the user should start with.
|
|
||||||
* If no accounts are configured the user is taken to the Accounts Activity where they
|
|
||||||
* can configure an account.
|
|
||||||
* If a single account is configured the user is taken directly to the FolderMessageList for
|
|
||||||
* the INBOX of that account.
|
|
||||||
* If more than one account is configuref the user is takaen to the Accounts Activity so they
|
|
||||||
* can select an account.
|
|
||||||
*/
|
|
||||||
public class Welcome extends Activity {
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle icicle) {
|
|
||||||
super.onCreate(icicle);
|
|
||||||
|
|
||||||
Account[] accounts = Preferences.getPreferences(this).getAccounts();
|
|
||||||
if (accounts.length == 1) {
|
|
||||||
FolderMessageList.actionHandleAccount(this, accounts[0], k9.INBOX);
|
|
||||||
} else {
|
|
||||||
startActivity(new Intent(this, Accounts.class));
|
|
||||||
}
|
|
||||||
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,192 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity.setup;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.preference.PreferenceActivity;
|
|
||||||
import android.preference.EditTextPreference;
|
|
||||||
import android.preference.ListPreference;
|
|
||||||
import android.preference.CheckBoxPreference;
|
|
||||||
import android.preference.Preference;
|
|
||||||
import android.preference.RingtonePreference;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.Preferences;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
|
|
||||||
public class AccountSettings extends PreferenceActivity {
|
|
||||||
private static final String EXTRA_ACCOUNT = "account";
|
|
||||||
|
|
||||||
private static final String PREFERENCE_TOP_CATERGORY = "account_settings";
|
|
||||||
private static final String PREFERENCE_DESCRIPTION = "account_description";
|
|
||||||
private static final String PREFERENCE_COMPOSITION = "composition";
|
|
||||||
private static final String PREFERENCE_FREQUENCY = "account_check_frequency";
|
|
||||||
private static final String PREFERENCE_DISPLAY_COUNT = "account_display_count";
|
|
||||||
private static final String PREFERENCE_DEFAULT = "account_default";
|
|
||||||
private static final String PREFERENCE_NOTIFY = "account_notify";
|
|
||||||
private static final String PREFERENCE_NOTIFY_RINGTONE = "account_notify_ringtone";
|
|
||||||
private static final String PREFERENCE_VIBRATE = "account_vibrate";
|
|
||||||
private static final String PREFERENCE_RINGTONE = "account_ringtone";
|
|
||||||
private static final String PREFERENCE_INCOMING = "incoming";
|
|
||||||
private static final String PREFERENCE_OUTGOING = "outgoing";
|
|
||||||
|
|
||||||
private Account mAccount;
|
|
||||||
|
|
||||||
private EditTextPreference mAccountDescription;
|
|
||||||
private ListPreference mCheckFrequency;
|
|
||||||
private ListPreference mDisplayCount;
|
|
||||||
private CheckBoxPreference mAccountDefault;
|
|
||||||
private CheckBoxPreference mAccountNotify;
|
|
||||||
private CheckBoxPreference mAccountNotifyRingtone;
|
|
||||||
private CheckBoxPreference mAccountVibrate;
|
|
||||||
private RingtonePreference mAccountRingtone;
|
|
||||||
|
|
||||||
public static void actionSettings(Context context, Account account) {
|
|
||||||
Intent i = new Intent(context, AccountSettings.class);
|
|
||||||
i.putExtra(EXTRA_ACCOUNT, account);
|
|
||||||
context.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
|
|
||||||
|
|
||||||
addPreferencesFromResource(R.xml.account_settings_preferences);
|
|
||||||
|
|
||||||
Preference category = findPreference(PREFERENCE_TOP_CATERGORY);
|
|
||||||
category.setTitle(getString(R.string.account_settings_title_fmt));
|
|
||||||
|
|
||||||
mAccountDescription = (EditTextPreference) findPreference(PREFERENCE_DESCRIPTION);
|
|
||||||
mAccountDescription.setSummary(mAccount.getDescription());
|
|
||||||
mAccountDescription.setText(mAccount.getDescription());
|
|
||||||
mAccountDescription.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
final String summary = newValue.toString();
|
|
||||||
mAccountDescription.setSummary(summary);
|
|
||||||
mAccountDescription.setText(summary);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
mCheckFrequency = (ListPreference) findPreference(PREFERENCE_FREQUENCY);
|
|
||||||
mCheckFrequency.setValue(String.valueOf(mAccount.getAutomaticCheckIntervalMinutes()));
|
|
||||||
mCheckFrequency.setSummary(mCheckFrequency.getEntry());
|
|
||||||
mCheckFrequency.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
final String summary = newValue.toString();
|
|
||||||
int index = mCheckFrequency.findIndexOfValue(summary);
|
|
||||||
mCheckFrequency.setSummary(mCheckFrequency.getEntries()[index]);
|
|
||||||
mCheckFrequency.setValue(summary);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mDisplayCount = (ListPreference) findPreference(PREFERENCE_DISPLAY_COUNT);
|
|
||||||
mDisplayCount.setValue(String.valueOf(mAccount.getDisplayCount()));
|
|
||||||
mDisplayCount.setSummary(mDisplayCount.getEntry());
|
|
||||||
mDisplayCount.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
|
|
||||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
|
||||||
final String summary = newValue.toString();
|
|
||||||
int index = mDisplayCount.findIndexOfValue(summary);
|
|
||||||
mDisplayCount.setSummary(mDisplayCount.getEntries()[index]);
|
|
||||||
mDisplayCount.setValue(summary);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
mAccountDefault = (CheckBoxPreference) findPreference(PREFERENCE_DEFAULT);
|
|
||||||
mAccountDefault.setChecked(
|
|
||||||
mAccount.equals(Preferences.getPreferences(this).getDefaultAccount()));
|
|
||||||
|
|
||||||
mAccountNotify = (CheckBoxPreference) findPreference(PREFERENCE_NOTIFY);
|
|
||||||
mAccountNotify.setChecked(mAccount.isNotifyNewMail());
|
|
||||||
|
|
||||||
mAccountNotifyRingtone = (CheckBoxPreference) findPreference(PREFERENCE_NOTIFY_RINGTONE);
|
|
||||||
mAccountNotifyRingtone.setChecked(mAccount.isNotifyRingtone());
|
|
||||||
|
|
||||||
mAccountRingtone = (RingtonePreference) findPreference(PREFERENCE_RINGTONE);
|
|
||||||
|
|
||||||
// XXX: The following two lines act as a workaround for the RingtonePreference
|
|
||||||
// which does not let us set/get the value programmatically
|
|
||||||
SharedPreferences prefs = mAccountRingtone.getPreferenceManager().getSharedPreferences();
|
|
||||||
prefs.edit().putString(PREFERENCE_RINGTONE, mAccount.getRingtone()).commit();
|
|
||||||
|
|
||||||
mAccountVibrate = (CheckBoxPreference) findPreference(PREFERENCE_VIBRATE);
|
|
||||||
mAccountVibrate.setChecked(mAccount.isVibrate());
|
|
||||||
|
|
||||||
|
|
||||||
findPreference(PREFERENCE_COMPOSITION).setOnPreferenceClickListener(
|
|
||||||
new Preference.OnPreferenceClickListener() {
|
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
|
||||||
onCompositionSettings();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
findPreference(PREFERENCE_INCOMING).setOnPreferenceClickListener(
|
|
||||||
new Preference.OnPreferenceClickListener() {
|
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
|
||||||
onIncomingSettings();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
findPreference(PREFERENCE_OUTGOING).setOnPreferenceClickListener(
|
|
||||||
new Preference.OnPreferenceClickListener() {
|
|
||||||
public boolean onPreferenceClick(Preference preference) {
|
|
||||||
onOutgoingSettings();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
mAccount.refresh(Preferences.getPreferences(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveSettings() {
|
|
||||||
if (mAccountDefault.isChecked()) {
|
|
||||||
Preferences.getPreferences(this).setDefaultAccount(mAccount);
|
|
||||||
}
|
|
||||||
mAccount.setDescription(mAccountDescription.getText());
|
|
||||||
mAccount.setNotifyNewMail(mAccountNotify.isChecked());
|
|
||||||
mAccount.setNotifyRingtone(mAccountNotifyRingtone.isChecked());
|
|
||||||
mAccount.setAutomaticCheckIntervalMinutes(Integer.parseInt(mCheckFrequency.getValue()));
|
|
||||||
mAccount.setDisplayCount(Integer.parseInt(mDisplayCount.getValue()));
|
|
||||||
mAccount.setVibrate(mAccountVibrate.isChecked());
|
|
||||||
SharedPreferences prefs = mAccountRingtone.getPreferenceManager().getSharedPreferences();
|
|
||||||
mAccount.setRingtone(prefs.getString(PREFERENCE_RINGTONE, null));
|
|
||||||
mAccount.save(Preferences.getPreferences(this));
|
|
||||||
k9.setServicesEnabled(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
|
||||||
saveSettings();
|
|
||||||
}
|
|
||||||
return super.onKeyDown(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onCompositionSettings() {
|
|
||||||
AccountSetupComposition.actionEditCompositionSettings(this, mAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onIncomingSettings() {
|
|
||||||
AccountSetupIncoming.actionEditIncomingSettings(this, mAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onOutgoingSettings() {
|
|
||||||
AccountSetupOutgoing.actionEditOutgoingSettings(this, mAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,109 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity.setup;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.widget.Button;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompts the user to select an account type. The account type, along with the
|
|
||||||
* passed in email address, password and makeDefault are then passed on to the
|
|
||||||
* AccountSetupIncoming activity.
|
|
||||||
*/
|
|
||||||
public class AccountSetupAccountType extends Activity implements OnClickListener {
|
|
||||||
private static final String EXTRA_ACCOUNT = "account";
|
|
||||||
|
|
||||||
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
|
|
||||||
|
|
||||||
private Account mAccount;
|
|
||||||
|
|
||||||
private boolean mMakeDefault;
|
|
||||||
|
|
||||||
public static void actionSelectAccountType(Context context, Account account, boolean makeDefault) {
|
|
||||||
Intent i = new Intent(context, AccountSetupAccountType.class);
|
|
||||||
i.putExtra(EXTRA_ACCOUNT, account);
|
|
||||||
i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault);
|
|
||||||
context.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.account_setup_account_type);
|
|
||||||
((Button)findViewById(R.id.pop)).setOnClickListener(this);
|
|
||||||
((Button)findViewById(R.id.imap)).setOnClickListener(this);
|
|
||||||
((Button)findViewById(R.id.webdav)).setOnClickListener(this);
|
|
||||||
|
|
||||||
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
|
|
||||||
mMakeDefault = (boolean)getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onPop() {
|
|
||||||
try {
|
|
||||||
URI uri = new URI(mAccount.getStoreUri());
|
|
||||||
uri = new URI("pop3", uri.getUserInfo(), uri.getHost(), uri.getPort(), null, null, null);
|
|
||||||
mAccount.setStoreUri(uri.toString());
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
/*
|
|
||||||
* This should not happen.
|
|
||||||
*/
|
|
||||||
throw new Error(use);
|
|
||||||
}
|
|
||||||
AccountSetupIncoming.actionIncomingSettings(this, mAccount, mMakeDefault);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onImap() {
|
|
||||||
try {
|
|
||||||
URI uri = new URI(mAccount.getStoreUri());
|
|
||||||
uri = new URI("imap", uri.getUserInfo(), uri.getHost(), uri.getPort(), null, null, null);
|
|
||||||
mAccount.setStoreUri(uri.toString());
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
/*
|
|
||||||
* This should not happen.
|
|
||||||
*/
|
|
||||||
throw new Error(use);
|
|
||||||
}
|
|
||||||
AccountSetupIncoming.actionIncomingSettings(this, mAccount, mMakeDefault);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onWebDav() {
|
|
||||||
try {
|
|
||||||
URI uri = new URI(mAccount.getStoreUri());
|
|
||||||
uri = new URI("webdav", uri.getUserInfo(), uri.getHost(), uri.getPort(), null, null, null);
|
|
||||||
mAccount.setStoreUri(uri.toString());
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
/*
|
|
||||||
* This should not happen.
|
|
||||||
*/
|
|
||||||
throw new Error(use);
|
|
||||||
}
|
|
||||||
AccountSetupIncoming.actionIncomingSettings(this, mAccount, mMakeDefault);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(View v) {
|
|
||||||
switch (v.getId()) {
|
|
||||||
case R.id.pop:
|
|
||||||
onPop();
|
|
||||||
break;
|
|
||||||
case R.id.imap:
|
|
||||||
onImap();
|
|
||||||
break;
|
|
||||||
case R.id.webdav:
|
|
||||||
onWebDav();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,388 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity.setup;
|
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.app.Dialog;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.XmlResourceParser;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.provider.Contacts;
|
|
||||||
import android.provider.Contacts.People.ContactMethods;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.EditText;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.EmailAddressValidator;
|
|
||||||
import com.fsck.k9.Preferences;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
import com.fsck.k9.Utility;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Prompts the user for the email address and password. Also prompts for
|
|
||||||
* "Use this account as default" if this is the 2nd+ account being set up.
|
|
||||||
* Attempts to lookup default settings for the domain the user specified. If the
|
|
||||||
* domain is known the settings are handed off to the AccountSetupCheckSettings
|
|
||||||
* activity. If no settings are found the settings are handed off to the
|
|
||||||
* AccountSetupAccountType activity.
|
|
||||||
*/
|
|
||||||
public class AccountSetupBasics extends Activity
|
|
||||||
implements OnClickListener, TextWatcher {
|
|
||||||
private final static String EXTRA_ACCOUNT = "com.fsck.k9.AccountSetupBasics.account";
|
|
||||||
private final static int DIALOG_NOTE = 1;
|
|
||||||
private final static String STATE_KEY_PROVIDER =
|
|
||||||
"com.fsck.k9.AccountSetupBasics.provider";
|
|
||||||
|
|
||||||
private Preferences mPrefs;
|
|
||||||
private EditText mEmailView;
|
|
||||||
private EditText mPasswordView;
|
|
||||||
private CheckBox mDefaultView;
|
|
||||||
private Button mNextButton;
|
|
||||||
private Button mManualSetupButton;
|
|
||||||
private Account mAccount;
|
|
||||||
private Provider mProvider;
|
|
||||||
|
|
||||||
private EmailAddressValidator mEmailValidator = new EmailAddressValidator();
|
|
||||||
|
|
||||||
public static void actionNewAccount(Context context) {
|
|
||||||
Intent i = new Intent(context, AccountSetupBasics.class);
|
|
||||||
context.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.account_setup_basics);
|
|
||||||
mPrefs = Preferences.getPreferences(this);
|
|
||||||
mEmailView = (EditText)findViewById(R.id.account_email);
|
|
||||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
|
||||||
mDefaultView = (CheckBox)findViewById(R.id.account_default);
|
|
||||||
mNextButton = (Button)findViewById(R.id.next);
|
|
||||||
mManualSetupButton = (Button)findViewById(R.id.manual_setup);
|
|
||||||
|
|
||||||
mNextButton.setOnClickListener(this);
|
|
||||||
mManualSetupButton.setOnClickListener(this);
|
|
||||||
|
|
||||||
mEmailView.addTextChangedListener(this);
|
|
||||||
mPasswordView.addTextChangedListener(this);
|
|
||||||
|
|
||||||
if (mPrefs.getAccounts().length > 0) {
|
|
||||||
mDefaultView.setVisibility(View.VISIBLE);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
|
|
||||||
mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(STATE_KEY_PROVIDER)) {
|
|
||||||
mProvider = (Provider)savedInstanceState.getSerializable(STATE_KEY_PROVIDER);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
validateFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putSerializable(EXTRA_ACCOUNT, mAccount);
|
|
||||||
if (mProvider != null) {
|
|
||||||
outState.putSerializable(STATE_KEY_PROVIDER, mProvider);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void afterTextChanged(Editable s) {
|
|
||||||
validateFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateFields() {
|
|
||||||
boolean valid = Utility.requiredFieldValid(mEmailView)
|
|
||||||
&& Utility.requiredFieldValid(mPasswordView)
|
|
||||||
&& mEmailValidator.isValid(mEmailView.getText().toString());
|
|
||||||
mNextButton.setEnabled(valid);
|
|
||||||
mManualSetupButton.setEnabled(valid);
|
|
||||||
/*
|
|
||||||
* Dim the next button's icon to 50% if the button is disabled.
|
|
||||||
* TODO this can probably be done with a stateful drawable. Check into it.
|
|
||||||
* android:state_enabled
|
|
||||||
*/
|
|
||||||
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String getOwnerName() {
|
|
||||||
String name = null;
|
|
||||||
String projection[] = {
|
|
||||||
ContactMethods.NAME
|
|
||||||
};
|
|
||||||
Cursor c = getContentResolver().query(
|
|
||||||
Uri.withAppendedPath(Contacts.People.CONTENT_URI, "owner"), projection, null, null,
|
|
||||||
null);
|
|
||||||
if (c.getCount() > 0) {
|
|
||||||
c.moveToFirst();
|
|
||||||
name = c.getString(0);
|
|
||||||
c.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (name == null || name.length() == 0) {
|
|
||||||
Account account = Preferences.getPreferences(this).getDefaultAccount();
|
|
||||||
if (account != null) {
|
|
||||||
name = account.getName();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Dialog onCreateDialog(int id) {
|
|
||||||
if (id == DIALOG_NOTE) {
|
|
||||||
if (mProvider != null && mProvider.note != null) {
|
|
||||||
return new AlertDialog.Builder(this)
|
|
||||||
.setMessage(mProvider.note)
|
|
||||||
.setPositiveButton(
|
|
||||||
getString(R.string.okay_action),
|
|
||||||
new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
finishAutoSetup();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton(
|
|
||||||
getString(R.string.cancel_action),
|
|
||||||
null)
|
|
||||||
.create();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void finishAutoSetup() {
|
|
||||||
String email = mEmailView.getText().toString();
|
|
||||||
String password = mPasswordView.getText().toString();
|
|
||||||
String[] emailParts = email.split("@");
|
|
||||||
String user = emailParts[0];
|
|
||||||
String domain = emailParts[1];
|
|
||||||
URI incomingUri = null;
|
|
||||||
URI outgoingUri = null;
|
|
||||||
try {
|
|
||||||
String incomingUsername = mProvider.incomingUsernameTemplate;
|
|
||||||
incomingUsername = incomingUsername.replaceAll("\\$email", email);
|
|
||||||
incomingUsername = incomingUsername.replaceAll("\\$user", user);
|
|
||||||
incomingUsername = incomingUsername.replaceAll("\\$domain", domain);
|
|
||||||
|
|
||||||
URI incomingUriTemplate = mProvider.incomingUriTemplate;
|
|
||||||
incomingUri = new URI(incomingUriTemplate.getScheme(), incomingUsername + ":"
|
|
||||||
+ password, incomingUriTemplate.getHost(), incomingUriTemplate.getPort(), null,
|
|
||||||
null, null);
|
|
||||||
|
|
||||||
String outgoingUsername = mProvider.outgoingUsernameTemplate;
|
|
||||||
outgoingUsername = outgoingUsername.replaceAll("\\$email", email);
|
|
||||||
outgoingUsername = outgoingUsername.replaceAll("\\$user", user);
|
|
||||||
outgoingUsername = outgoingUsername.replaceAll("\\$domain", domain);
|
|
||||||
|
|
||||||
URI outgoingUriTemplate = mProvider.outgoingUriTemplate;
|
|
||||||
outgoingUri = new URI(outgoingUriTemplate.getScheme(), outgoingUsername + ":"
|
|
||||||
+ password, outgoingUriTemplate.getHost(), outgoingUriTemplate.getPort(), null,
|
|
||||||
null, null);
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
/*
|
|
||||||
* If there is some problem with the URI we give up and go on to
|
|
||||||
* manual setup.
|
|
||||||
*/
|
|
||||||
onManualSetup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mAccount = new Account(this);
|
|
||||||
mAccount.setName(getOwnerName());
|
|
||||||
mAccount.setEmail(email);
|
|
||||||
mAccount.setStoreUri(incomingUri.toString());
|
|
||||||
mAccount.setTransportUri(outgoingUri.toString());
|
|
||||||
mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
|
|
||||||
mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
|
|
||||||
mAccount.setOutboxFolderName(getString(R.string.special_mailbox_name_outbox));
|
|
||||||
mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
|
|
||||||
if (incomingUri.toString().startsWith("imap")) {
|
|
||||||
mAccount.setDeletePolicy(Account.DELETE_POLICY_ON_DELETE);
|
|
||||||
}
|
|
||||||
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onNext() {
|
|
||||||
String email = mEmailView.getText().toString();
|
|
||||||
String password = mPasswordView.getText().toString();
|
|
||||||
String[] emailParts = email.split("@");
|
|
||||||
String user = emailParts[0];
|
|
||||||
String domain = emailParts[1];
|
|
||||||
mProvider = findProviderForDomain(domain);
|
|
||||||
if (mProvider == null) {
|
|
||||||
/*
|
|
||||||
* We don't have default settings for this account, start the manual
|
|
||||||
* setup process.
|
|
||||||
*/
|
|
||||||
onManualSetup();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mProvider.note != null) {
|
|
||||||
showDialog(DIALOG_NOTE);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
finishAutoSetup();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
mAccount.setDescription(mAccount.getEmail());
|
|
||||||
mAccount.save(Preferences.getPreferences(this));
|
|
||||||
if (mDefaultView.isChecked()) {
|
|
||||||
Preferences.getPreferences(this).setDefaultAccount(mAccount);
|
|
||||||
}
|
|
||||||
k9.setServicesEnabled(this);
|
|
||||||
AccountSetupNames.actionSetNames(this, mAccount);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onManualSetup() {
|
|
||||||
String email = mEmailView.getText().toString();
|
|
||||||
String password = mPasswordView.getText().toString();
|
|
||||||
String[] emailParts = email.split("@");
|
|
||||||
String user = emailParts[0];
|
|
||||||
String domain = emailParts[1];
|
|
||||||
|
|
||||||
mAccount = new Account(this);
|
|
||||||
mAccount.setName(getOwnerName());
|
|
||||||
mAccount.setEmail(email);
|
|
||||||
try {
|
|
||||||
URI uri = new URI("placeholder", user + ":" + password, "mail." + domain, -1, null,
|
|
||||||
null, null);
|
|
||||||
mAccount.setStoreUri(uri.toString());
|
|
||||||
mAccount.setTransportUri(uri.toString());
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
/*
|
|
||||||
* If we can't set up the URL we just continue. It's only for
|
|
||||||
* convenience.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
mAccount.setDraftsFolderName(getString(R.string.special_mailbox_name_drafts));
|
|
||||||
mAccount.setTrashFolderName(getString(R.string.special_mailbox_name_trash));
|
|
||||||
mAccount.setOutboxFolderName(getString(R.string.special_mailbox_name_outbox));
|
|
||||||
mAccount.setSentFolderName(getString(R.string.special_mailbox_name_sent));
|
|
||||||
|
|
||||||
AccountSetupAccountType.actionSelectAccountType(this, mAccount, mDefaultView.isChecked());
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(View v) {
|
|
||||||
switch (v.getId()) {
|
|
||||||
case R.id.next:
|
|
||||||
onNext();
|
|
||||||
break;
|
|
||||||
case R.id.manual_setup:
|
|
||||||
onManualSetup();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempts to get the given attribute as a String resource first, and if it fails
|
|
||||||
* returns the attribute as a simple String value.
|
|
||||||
* @param xml
|
|
||||||
* @param name
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
private String getXmlAttribute(XmlResourceParser xml, String name) {
|
|
||||||
int resId = xml.getAttributeResourceValue(null, name, 0);
|
|
||||||
if (resId == 0) {
|
|
||||||
return xml.getAttributeValue(null, name);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return getString(resId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Provider findProviderForDomain(String domain) {
|
|
||||||
try {
|
|
||||||
XmlResourceParser xml = getResources().getXml(R.xml.providers);
|
|
||||||
int xmlEventType;
|
|
||||||
Provider provider = null;
|
|
||||||
while ((xmlEventType = xml.next()) != XmlResourceParser.END_DOCUMENT) {
|
|
||||||
if (xmlEventType == XmlResourceParser.START_TAG
|
|
||||||
&& "provider".equals(xml.getName())
|
|
||||||
&& domain.equalsIgnoreCase(getXmlAttribute(xml, "domain"))) {
|
|
||||||
provider = new Provider();
|
|
||||||
provider.id = getXmlAttribute(xml, "id");
|
|
||||||
provider.label = getXmlAttribute(xml, "label");
|
|
||||||
provider.domain = getXmlAttribute(xml, "domain");
|
|
||||||
provider.note = getXmlAttribute(xml, "note");
|
|
||||||
}
|
|
||||||
else if (xmlEventType == XmlResourceParser.START_TAG
|
|
||||||
&& "incoming".equals(xml.getName())
|
|
||||||
&& provider != null) {
|
|
||||||
provider.incomingUriTemplate = new URI(getXmlAttribute(xml, "uri"));
|
|
||||||
provider.incomingUsernameTemplate = getXmlAttribute(xml, "username");
|
|
||||||
}
|
|
||||||
else if (xmlEventType == XmlResourceParser.START_TAG
|
|
||||||
&& "outgoing".equals(xml.getName())
|
|
||||||
&& provider != null) {
|
|
||||||
provider.outgoingUriTemplate = new URI(getXmlAttribute(xml, "uri"));
|
|
||||||
provider.outgoingUsernameTemplate = getXmlAttribute(xml, "username");
|
|
||||||
}
|
|
||||||
else if (xmlEventType == XmlResourceParser.END_TAG
|
|
||||||
&& "provider".equals(xml.getName())
|
|
||||||
&& provider != null) {
|
|
||||||
return provider;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
Log.e(k9.LOG_TAG, "Error while trying to load provider settings.", e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static class Provider implements Serializable {
|
|
||||||
private static final long serialVersionUID = 8511656164616538989L;
|
|
||||||
|
|
||||||
public String id;
|
|
||||||
|
|
||||||
public String label;
|
|
||||||
|
|
||||||
public String domain;
|
|
||||||
|
|
||||||
public URI incomingUriTemplate;
|
|
||||||
|
|
||||||
public String incomingUsernameTemplate;
|
|
||||||
|
|
||||||
public URI outgoingUriTemplate;
|
|
||||||
|
|
||||||
public String outgoingUsernameTemplate;
|
|
||||||
|
|
||||||
public String note;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,274 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity.setup;
|
|
||||||
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Process;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
import com.fsck.k9.mail.AuthenticationFailedException;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
import com.fsck.k9.mail.Store;
|
|
||||||
import com.fsck.k9.mail.Transport;
|
|
||||||
import com.fsck.k9.mail.CertificateValidationException;
|
|
||||||
import com.fsck.k9.mail.store.TrustManagerFactory;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks the given settings to make sure that they can be used to send and
|
|
||||||
* receive mail.
|
|
||||||
*
|
|
||||||
* XXX NOTE: The manifest for this app has it ignore config changes, because
|
|
||||||
* it doesn't correctly deal with restarting while its thread is running.
|
|
||||||
*/
|
|
||||||
public class AccountSetupCheckSettings extends Activity implements OnClickListener {
|
|
||||||
private static final String EXTRA_ACCOUNT = "account";
|
|
||||||
|
|
||||||
private static final String EXTRA_CHECK_INCOMING = "checkIncoming";
|
|
||||||
|
|
||||||
private static final String EXTRA_CHECK_OUTGOING = "checkOutgoing";
|
|
||||||
|
|
||||||
private Handler mHandler = new Handler();
|
|
||||||
|
|
||||||
private ProgressBar mProgressBar;
|
|
||||||
|
|
||||||
private TextView mMessageView;
|
|
||||||
|
|
||||||
private Account mAccount;
|
|
||||||
|
|
||||||
private boolean mCheckIncoming;
|
|
||||||
|
|
||||||
private boolean mCheckOutgoing;
|
|
||||||
|
|
||||||
private boolean mCanceled;
|
|
||||||
|
|
||||||
private boolean mDestroyed;
|
|
||||||
|
|
||||||
public static void actionCheckSettings(Activity context, Account account,
|
|
||||||
boolean checkIncoming, boolean checkOutgoing) {
|
|
||||||
Intent i = new Intent(context, AccountSetupCheckSettings.class);
|
|
||||||
i.putExtra(EXTRA_ACCOUNT, account);
|
|
||||||
i.putExtra(EXTRA_CHECK_INCOMING, checkIncoming);
|
|
||||||
i.putExtra(EXTRA_CHECK_OUTGOING, checkOutgoing);
|
|
||||||
context.startActivityForResult(i, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.account_setup_check_settings);
|
|
||||||
mMessageView = (TextView)findViewById(R.id.message);
|
|
||||||
mProgressBar = (ProgressBar)findViewById(R.id.progress);
|
|
||||||
((Button)findViewById(R.id.cancel)).setOnClickListener(this);
|
|
||||||
|
|
||||||
setMessage(R.string.account_setup_check_settings_retr_info_msg);
|
|
||||||
mProgressBar.setIndeterminate(true);
|
|
||||||
|
|
||||||
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
|
|
||||||
mCheckIncoming = (boolean)getIntent().getBooleanExtra(EXTRA_CHECK_INCOMING, false);
|
|
||||||
mCheckOutgoing = (boolean)getIntent().getBooleanExtra(EXTRA_CHECK_OUTGOING, false);
|
|
||||||
|
|
||||||
new Thread() {
|
|
||||||
public void run() {
|
|
||||||
Store store = null;
|
|
||||||
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
|
|
||||||
try {
|
|
||||||
if (mDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mCanceled) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mCheckIncoming) {
|
|
||||||
setMessage(R.string.account_setup_check_settings_check_incoming_msg);
|
|
||||||
store = Store.getInstance(mAccount.getStoreUri(), getApplication());
|
|
||||||
store.checkSettings();
|
|
||||||
}
|
|
||||||
if (mDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mCanceled) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mCheckOutgoing) {
|
|
||||||
setMessage(R.string.account_setup_check_settings_check_outgoing_msg);
|
|
||||||
Transport transport = Transport.getInstance(mAccount.getTransportUri());
|
|
||||||
transport.close();
|
|
||||||
transport.open();
|
|
||||||
transport.close();
|
|
||||||
}
|
|
||||||
if (mDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (mCanceled) {
|
|
||||||
finish();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setResult(RESULT_OK);
|
|
||||||
finish();
|
|
||||||
} catch (final AuthenticationFailedException afe) {
|
|
||||||
showErrorDialog(
|
|
||||||
R.string.account_setup_failed_dlg_auth_message_fmt,
|
|
||||||
afe.getMessage() == null ? "" : afe.getMessage());
|
|
||||||
} catch (final CertificateValidationException cve) {
|
|
||||||
acceptKeyDialog(
|
|
||||||
R.string.account_setup_failed_dlg_certificate_message_fmt,
|
|
||||||
cve);
|
|
||||||
//cve.getMessage() == null ? "" : cve.getMessage());
|
|
||||||
} catch (final MessagingException me) {
|
|
||||||
showErrorDialog(
|
|
||||||
R.string.account_setup_failed_dlg_server_message_fmt,
|
|
||||||
me.getMessage() == null ? "" : me.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
mDestroyed = true;
|
|
||||||
mCanceled = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setMessage(final int resId) {
|
|
||||||
mHandler.post(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
if (mDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mMessageView.setText(getString(resId));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void showErrorDialog(final int msgResId, final Object... args) {
|
|
||||||
mHandler.post(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
if (mDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mProgressBar.setIndeterminate(false);
|
|
||||||
new AlertDialog.Builder(AccountSetupCheckSettings.this)
|
|
||||||
.setTitle(getString(R.string.account_setup_failed_dlg_title))
|
|
||||||
.setMessage(getString(msgResId, args))
|
|
||||||
.setCancelable(true)
|
|
||||||
.setPositiveButton(
|
|
||||||
getString(R.string.account_setup_failed_dlg_edit_details_action),
|
|
||||||
new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
private void acceptKeyDialog(final int msgResId, final Object... args) {
|
|
||||||
mHandler.post(new Runnable() {
|
|
||||||
public void run() {
|
|
||||||
if (mDestroyed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final X509Certificate[] chain = TrustManagerFactory.getLastCertChain();
|
|
||||||
String exMessage = "Unknown Error";
|
|
||||||
|
|
||||||
Exception ex = ((Exception)args[0]);
|
|
||||||
if (ex != null) {
|
|
||||||
if (ex.getCause() != null) {
|
|
||||||
if (ex.getCause().getCause() != null) {
|
|
||||||
exMessage = ex.getCause().getCause().getMessage();
|
|
||||||
|
|
||||||
} else {
|
|
||||||
exMessage = ex.getCause().getMessage();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
exMessage = ex.getMessage();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mProgressBar.setIndeterminate(false);
|
|
||||||
StringBuffer chainInfo = new StringBuffer(100);
|
|
||||||
for (int i = 0; i < chain.length; i++)
|
|
||||||
{
|
|
||||||
// display certificate chain information
|
|
||||||
chainInfo.append("Certificate chain[" + i + "]:\n");
|
|
||||||
chainInfo.append("Subject: " + chain[i].getSubjectDN().toString() + "\n");
|
|
||||||
chainInfo.append("Issuer: " + chain[i].getIssuerDN().toString() + "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
new AlertDialog.Builder(AccountSetupCheckSettings.this)
|
|
||||||
.setTitle(getString(R.string.account_setup_failed_dlg_invalid_certificate_title))
|
|
||||||
//.setMessage(getString(R.string.account_setup_failed_dlg_invalid_certificate)
|
|
||||||
.setMessage(getString(msgResId,exMessage)
|
|
||||||
+ " " + chainInfo.toString()
|
|
||||||
)
|
|
||||||
.setCancelable(true)
|
|
||||||
.setPositiveButton(
|
|
||||||
getString(R.string.account_setup_failed_dlg_invalid_certificate_accept),
|
|
||||||
new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
try {
|
|
||||||
String alias = mAccount.getUuid();
|
|
||||||
if (mCheckIncoming) {
|
|
||||||
alias = alias + ".incoming";
|
|
||||||
}
|
|
||||||
if (mCheckOutgoing) {
|
|
||||||
alias = alias + ".outgoing";
|
|
||||||
}
|
|
||||||
TrustManagerFactory.addCertificateChain(alias, chain);
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
showErrorDialog(
|
|
||||||
R.string.account_setup_failed_dlg_certificate_message_fmt,
|
|
||||||
e.getMessage() == null ? "" : e.getMessage());
|
|
||||||
}
|
|
||||||
AccountSetupCheckSettings.actionCheckSettings(AccountSetupCheckSettings.this, mAccount,
|
|
||||||
mCheckIncoming, mCheckOutgoing);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.setNegativeButton(
|
|
||||||
getString(R.string.account_setup_failed_dlg_invalid_certificate_reject),
|
|
||||||
new DialogInterface.OnClickListener() {
|
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onActivityResult(int reqCode, int resCode, Intent data) {
|
|
||||||
setResult(resCode);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private void onCancel() {
|
|
||||||
mCanceled = true;
|
|
||||||
setMessage(R.string.account_setup_check_settings_canceling_msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(View v) {
|
|
||||||
switch (v.getId()) {
|
|
||||||
case R.id.cancel:
|
|
||||||
onCancel();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,117 +0,0 @@
|
|||||||
package com.fsck.k9.activity.setup;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.KeyEvent;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.Preferences;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.Utility;
|
|
||||||
|
|
||||||
public class AccountSetupComposition extends Activity {
|
|
||||||
|
|
||||||
private static final String EXTRA_ACCOUNT = "account";
|
|
||||||
|
|
||||||
private Account mAccount;
|
|
||||||
|
|
||||||
private EditText mAccountSignature;
|
|
||||||
private EditText mAccountEmail;
|
|
||||||
private EditText mAccountAlwaysBcc;
|
|
||||||
private EditText mAccountName;
|
|
||||||
private EditText mAccountSentItems;
|
|
||||||
private EditText mAccountDeletedItems;
|
|
||||||
|
|
||||||
|
|
||||||
public static void actionEditCompositionSettings(Activity context, Account account) {
|
|
||||||
Intent i = new Intent(context, AccountSetupComposition.class);
|
|
||||||
i.setAction(Intent.ACTION_EDIT);
|
|
||||||
i.putExtra(EXTRA_ACCOUNT, account);
|
|
||||||
context.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
|
|
||||||
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
|
|
||||||
|
|
||||||
setContentView(R.layout.account_setup_composition);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If we're being reloaded we override the original account with the one
|
|
||||||
* we saved
|
|
||||||
*/
|
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
|
|
||||||
mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
mAccountName = (EditText)findViewById(R.id.account_name);
|
|
||||||
mAccountName.setText(mAccount.getName());
|
|
||||||
|
|
||||||
mAccountEmail = (EditText)findViewById(R.id.account_email);
|
|
||||||
mAccountEmail.setText(mAccount.getEmail());
|
|
||||||
|
|
||||||
mAccountAlwaysBcc = (EditText)findViewById(R.id.account_always_bcc);
|
|
||||||
mAccountAlwaysBcc.setText(mAccount.getAlwaysBcc());
|
|
||||||
|
|
||||||
mAccountSignature = (EditText)findViewById(R.id.account_signature);
|
|
||||||
mAccountSignature.setText(mAccount.getSignature());
|
|
||||||
|
|
||||||
mAccountSentItems = (EditText)findViewById(R.id.account_sent_items);
|
|
||||||
mAccountSentItems.setText(mAccount.getSentFolderName());
|
|
||||||
|
|
||||||
mAccountDeletedItems = (EditText)findViewById(R.id.account_deleted_items);
|
|
||||||
mAccountDeletedItems.setText(mAccount.getTrashFolderName());
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
mAccount.refresh(Preferences.getPreferences(this));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saveSettings() {
|
|
||||||
mAccount.setEmail(mAccountEmail.getText().toString());
|
|
||||||
mAccount.setAlwaysBcc(mAccountAlwaysBcc.getText().toString());
|
|
||||||
mAccount.setName(mAccountName.getText().toString());
|
|
||||||
mAccount.setSignature(mAccountSignature.getText().toString());
|
|
||||||
mAccount.setSentFolderName(mAccountSentItems.getText().toString());
|
|
||||||
mAccount.setTrashFolderName(mAccountDeletedItems.getText().toString());
|
|
||||||
|
|
||||||
mAccount.save(Preferences.getPreferences(this));
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_BACK) {
|
|
||||||
saveSettings();
|
|
||||||
}
|
|
||||||
return super.onKeyDown(keyCode, event);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putSerializable(EXTRA_ACCOUNT, mAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
mAccount.save(Preferences.getPreferences(this));
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,338 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity.setup;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.text.method.DigitsKeyListener;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.Preferences;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
import com.fsck.k9.Utility;
|
|
||||||
|
|
||||||
public class AccountSetupIncoming extends Activity implements OnClickListener {
|
|
||||||
private static final String EXTRA_ACCOUNT = "account";
|
|
||||||
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
|
|
||||||
|
|
||||||
private static final int popPorts[] = {
|
|
||||||
110, 995, 995, 110, 110
|
|
||||||
};
|
|
||||||
private static final String popSchemes[] = {
|
|
||||||
"pop3", "pop3+ssl", "pop3+ssl+", "pop3+tls", "pop3+tls+"
|
|
||||||
};
|
|
||||||
private static final int imapPorts[] = {
|
|
||||||
143, 993, 993, 143, 143
|
|
||||||
};
|
|
||||||
private static final String imapSchemes[] = {
|
|
||||||
"imap", "imap+ssl", "imap+ssl+", "imap+tls", "imap+tls+"
|
|
||||||
};
|
|
||||||
private static final int webdavPorts[] = {
|
|
||||||
80, 443, 443, 443, 443
|
|
||||||
};
|
|
||||||
private static final String webdavSchemes[] = {
|
|
||||||
"webdav", "webdav+ssl", "webdav+ssl+", "webdav+tls", "webdav+tls+"
|
|
||||||
};
|
|
||||||
|
|
||||||
private int mAccountPorts[];
|
|
||||||
private String mAccountSchemes[];
|
|
||||||
private EditText mUsernameView;
|
|
||||||
private EditText mPasswordView;
|
|
||||||
private EditText mServerView;
|
|
||||||
private EditText mPortView;
|
|
||||||
private Spinner mSecurityTypeView;
|
|
||||||
private Spinner mDeletePolicyView;
|
|
||||||
private EditText mImapPathPrefixView;
|
|
||||||
private Button mNextButton;
|
|
||||||
private Account mAccount;
|
|
||||||
private boolean mMakeDefault;
|
|
||||||
|
|
||||||
public static void actionIncomingSettings(Activity context, Account account, boolean makeDefault) {
|
|
||||||
Intent i = new Intent(context, AccountSetupIncoming.class);
|
|
||||||
i.putExtra(EXTRA_ACCOUNT, account);
|
|
||||||
i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault);
|
|
||||||
context.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void actionEditIncomingSettings(Activity context, Account account) {
|
|
||||||
Intent i = new Intent(context, AccountSetupIncoming.class);
|
|
||||||
i.setAction(Intent.ACTION_EDIT);
|
|
||||||
i.putExtra(EXTRA_ACCOUNT, account);
|
|
||||||
context.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.account_setup_incoming);
|
|
||||||
|
|
||||||
mUsernameView = (EditText)findViewById(R.id.account_username);
|
|
||||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
|
||||||
TextView serverLabelView = (TextView) findViewById(R.id.account_server_label);
|
|
||||||
mServerView = (EditText)findViewById(R.id.account_server);
|
|
||||||
mPortView = (EditText)findViewById(R.id.account_port);
|
|
||||||
mSecurityTypeView = (Spinner)findViewById(R.id.account_security_type);
|
|
||||||
mDeletePolicyView = (Spinner)findViewById(R.id.account_delete_policy);
|
|
||||||
mImapPathPrefixView = (EditText)findViewById(R.id.imap_path_prefix);
|
|
||||||
mNextButton = (Button)findViewById(R.id.next);
|
|
||||||
|
|
||||||
mNextButton.setOnClickListener(this);
|
|
||||||
|
|
||||||
SpinnerOption securityTypes[] = {
|
|
||||||
new SpinnerOption(0, getString(R.string.account_setup_incoming_security_none_label)),
|
|
||||||
new SpinnerOption(1,
|
|
||||||
getString(R.string.account_setup_incoming_security_ssl_optional_label)),
|
|
||||||
new SpinnerOption(2, getString(R.string.account_setup_incoming_security_ssl_label)),
|
|
||||||
new SpinnerOption(3,
|
|
||||||
getString(R.string.account_setup_incoming_security_tls_optional_label)),
|
|
||||||
new SpinnerOption(4, getString(R.string.account_setup_incoming_security_tls_label)),
|
|
||||||
};
|
|
||||||
|
|
||||||
SpinnerOption deletePolicies[] = {
|
|
||||||
new SpinnerOption(0,
|
|
||||||
getString(R.string.account_setup_incoming_delete_policy_never_label)),
|
|
||||||
new SpinnerOption(1,
|
|
||||||
getString(R.string.account_setup_incoming_delete_policy_7days_label)),
|
|
||||||
new SpinnerOption(2,
|
|
||||||
getString(R.string.account_setup_incoming_delete_policy_delete_label)),
|
|
||||||
};
|
|
||||||
|
|
||||||
ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(this,
|
|
||||||
android.R.layout.simple_spinner_item, securityTypes);
|
|
||||||
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
mSecurityTypeView.setAdapter(securityTypesAdapter);
|
|
||||||
|
|
||||||
ArrayAdapter<SpinnerOption> deletePoliciesAdapter = new ArrayAdapter<SpinnerOption>(this,
|
|
||||||
android.R.layout.simple_spinner_item, deletePolicies);
|
|
||||||
deletePoliciesAdapter
|
|
||||||
.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
mDeletePolicyView.setAdapter(deletePoliciesAdapter);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Updates the port when the user changes the security type. This allows
|
|
||||||
* us to show a reasonable default which the user can change.
|
|
||||||
*/
|
|
||||||
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
|
||||||
public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) {
|
|
||||||
updatePortFromSecurityType();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onNothingSelected(AdapterView<?> arg0) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Calls validateFields() which enables or disables the Next button
|
|
||||||
* based on the fields' validity.
|
|
||||||
*/
|
|
||||||
TextWatcher validationTextWatcher = new TextWatcher() {
|
|
||||||
public void afterTextChanged(Editable s) {
|
|
||||||
validateFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mUsernameView.addTextChangedListener(validationTextWatcher);
|
|
||||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
|
||||||
mServerView.addTextChangedListener(validationTextWatcher);
|
|
||||||
mPortView.addTextChangedListener(validationTextWatcher);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Only allow digits in the port field.
|
|
||||||
*/
|
|
||||||
mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
|
|
||||||
|
|
||||||
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
|
|
||||||
mMakeDefault = (boolean)getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If we're being reloaded we override the original account with the one
|
|
||||||
* we saved
|
|
||||||
*/
|
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
|
|
||||||
mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
URI uri = new URI(mAccount.getStoreUri());
|
|
||||||
String username = null;
|
|
||||||
String password = null;
|
|
||||||
if (uri.getUserInfo() != null) {
|
|
||||||
String[] userInfoParts = uri.getUserInfo().split(":", 2);
|
|
||||||
username = userInfoParts[0];
|
|
||||||
if (userInfoParts.length > 1) {
|
|
||||||
password = userInfoParts[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username != null) {
|
|
||||||
mUsernameView.setText(username);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password != null) {
|
|
||||||
mPasswordView.setText(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri.getScheme().startsWith("pop3")) {
|
|
||||||
serverLabelView.setText(R.string.account_setup_incoming_pop_server_label);
|
|
||||||
mAccountPorts = popPorts;
|
|
||||||
mAccountSchemes = popSchemes;
|
|
||||||
|
|
||||||
findViewById(R.id.imap_path_prefix_section).setVisibility(View.GONE);
|
|
||||||
} else if (uri.getScheme().startsWith("imap")) {
|
|
||||||
serverLabelView.setText(R.string.account_setup_incoming_imap_server_label);
|
|
||||||
mAccountPorts = imapPorts;
|
|
||||||
mAccountSchemes = imapSchemes;
|
|
||||||
|
|
||||||
findViewById(R.id.account_delete_policy_label).setVisibility(View.GONE);
|
|
||||||
mDeletePolicyView.setVisibility(View.GONE);
|
|
||||||
if (uri.getPath() != null && uri.getPath().length() > 0) {
|
|
||||||
mImapPathPrefixView.setText(uri.getPath().substring(1));
|
|
||||||
}
|
|
||||||
} else if (uri.getScheme().startsWith("webdav")) {
|
|
||||||
serverLabelView.setText(R.string.account_setup_incoming_webdav_server_label);
|
|
||||||
mAccountPorts = webdavPorts;
|
|
||||||
mAccountSchemes = webdavSchemes;
|
|
||||||
|
|
||||||
/** Hide the unnecessary fields */
|
|
||||||
findViewById(R.id.imap_path_prefix_section).setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
throw new Error("Unknown account type: " + mAccount.getStoreUri());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < mAccountSchemes.length; i++) {
|
|
||||||
if (mAccountSchemes[i].equals(uri.getScheme())) {
|
|
||||||
SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SpinnerOption.setSpinnerOptionValue(mDeletePolicyView, mAccount.getDeletePolicy());
|
|
||||||
|
|
||||||
if (uri.getHost() != null) {
|
|
||||||
mServerView.setText(uri.getHost());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri.getPort() != -1) {
|
|
||||||
mPortView.setText(Integer.toString(uri.getPort()));
|
|
||||||
} else {
|
|
||||||
updatePortFromSecurityType();
|
|
||||||
}
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
/*
|
|
||||||
* We should always be able to parse our own settings.
|
|
||||||
*/
|
|
||||||
throw new Error(use);
|
|
||||||
}
|
|
||||||
|
|
||||||
validateFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putSerializable(EXTRA_ACCOUNT, mAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateFields() {
|
|
||||||
mNextButton
|
|
||||||
.setEnabled(Utility.requiredFieldValid(mUsernameView)
|
|
||||||
&& Utility.requiredFieldValid(mPasswordView)
|
|
||||||
&& Utility.requiredFieldValid(mServerView)
|
|
||||||
&& Utility.requiredFieldValid(mPortView));
|
|
||||||
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updatePortFromSecurityType() {
|
|
||||||
int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
|
|
||||||
mPortView.setText(Integer.toString(mAccountPorts[securityType]));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
if (Intent.ACTION_EDIT.equals(getIntent().getAction())) {
|
|
||||||
mAccount.save(Preferences.getPreferences(this));
|
|
||||||
finish();
|
|
||||||
} else {
|
|
||||||
/*
|
|
||||||
* Set the username and password for the outgoing settings to the username and
|
|
||||||
* password the user just set for incoming.
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
URI oldUri = new URI(mAccount.getTransportUri());
|
|
||||||
URI uri = new URI(
|
|
||||||
oldUri.getScheme(),
|
|
||||||
mUsernameView.getText() + ":" + mPasswordView.getText(),
|
|
||||||
oldUri.getHost(),
|
|
||||||
oldUri.getPort(),
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null);
|
|
||||||
mAccount.setTransportUri(uri.toString());
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
/*
|
|
||||||
* If we can't set up the URL we just continue. It's only for
|
|
||||||
* convenience.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
AccountSetupOutgoing.actionOutgoingSettings(this, mAccount, mMakeDefault);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onNext() {
|
|
||||||
int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
|
|
||||||
try {
|
|
||||||
String path = null;
|
|
||||||
if (mAccountSchemes[securityType].startsWith("imap")) {
|
|
||||||
path = "/" + mImapPathPrefixView.getText();
|
|
||||||
}
|
|
||||||
URI uri = new URI(
|
|
||||||
mAccountSchemes[securityType],
|
|
||||||
mUsernameView.getText() + ":" + mPasswordView.getText(),
|
|
||||||
mServerView.getText().toString(),
|
|
||||||
Integer.parseInt(mPortView.getText().toString()),
|
|
||||||
path, // path
|
|
||||||
null, // query
|
|
||||||
null);
|
|
||||||
mAccount.setStoreUri(uri.toString());
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
/*
|
|
||||||
* It's unrecoverable if we cannot create a URI from components that
|
|
||||||
* we validated to be safe.
|
|
||||||
*/
|
|
||||||
throw new Error(use);
|
|
||||||
}
|
|
||||||
|
|
||||||
mAccount.setDeletePolicy((Integer)((SpinnerOption)mDeletePolicyView.getSelectedItem()).value);
|
|
||||||
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, true, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(View v) {
|
|
||||||
switch (v.getId()) {
|
|
||||||
case R.id.next:
|
|
||||||
onNext();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,103 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity.setup;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.text.method.TextKeyListener;
|
|
||||||
import android.text.method.TextKeyListener.Capitalize;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.EditText;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.Preferences;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
import com.fsck.k9.Utility;
|
|
||||||
import com.fsck.k9.activity.FolderMessageList;
|
|
||||||
|
|
||||||
public class AccountSetupNames extends Activity implements OnClickListener {
|
|
||||||
private static final String EXTRA_ACCOUNT = "account";
|
|
||||||
|
|
||||||
private EditText mDescription;
|
|
||||||
|
|
||||||
private EditText mName;
|
|
||||||
|
|
||||||
private Account mAccount;
|
|
||||||
|
|
||||||
private Button mDoneButton;
|
|
||||||
|
|
||||||
public static void actionSetNames(Context context, Account account) {
|
|
||||||
Intent i = new Intent(context, AccountSetupNames.class);
|
|
||||||
i.putExtra(EXTRA_ACCOUNT, account);
|
|
||||||
context.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.account_setup_names);
|
|
||||||
mDescription = (EditText)findViewById(R.id.account_description);
|
|
||||||
mName = (EditText)findViewById(R.id.account_name);
|
|
||||||
mDoneButton = (Button)findViewById(R.id.done);
|
|
||||||
mDoneButton.setOnClickListener(this);
|
|
||||||
|
|
||||||
TextWatcher validationTextWatcher = new TextWatcher() {
|
|
||||||
public void afterTextChanged(Editable s) {
|
|
||||||
validateFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mName.addTextChangedListener(validationTextWatcher);
|
|
||||||
|
|
||||||
mName.setKeyListener(TextKeyListener.getInstance(false, Capitalize.WORDS));
|
|
||||||
|
|
||||||
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Since this field is considered optional, we don't set this here. If
|
|
||||||
* the user fills in a value we'll reset the current value, otherwise we
|
|
||||||
* just leave the saved value alone.
|
|
||||||
*/
|
|
||||||
// mDescription.setText(mAccount.getDescription());
|
|
||||||
if (mAccount.getName() != null) {
|
|
||||||
mName.setText(mAccount.getName());
|
|
||||||
}
|
|
||||||
if (!Utility.requiredFieldValid(mName)) {
|
|
||||||
mDoneButton.setEnabled(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateFields() {
|
|
||||||
mDoneButton.setEnabled(Utility.requiredFieldValid(mName));
|
|
||||||
Utility.setCompoundDrawablesAlpha(mDoneButton, mDoneButton.isEnabled() ? 255 : 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onNext() {
|
|
||||||
if (Utility.requiredFieldValid(mDescription)) {
|
|
||||||
mAccount.setDescription(mDescription.getText().toString());
|
|
||||||
}
|
|
||||||
mAccount.setName(mName.getText().toString());
|
|
||||||
mAccount.save(Preferences.getPreferences(this));
|
|
||||||
FolderMessageList.actionHandleAccount(this, mAccount, k9.INBOX);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(View v) {
|
|
||||||
switch (v.getId()) {
|
|
||||||
case R.id.done:
|
|
||||||
onNext();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,130 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity.setup;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.Preferences;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
|
|
||||||
public class AccountSetupOptions extends Activity implements OnClickListener {
|
|
||||||
private static final String EXTRA_ACCOUNT = "account";
|
|
||||||
|
|
||||||
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
|
|
||||||
|
|
||||||
private Spinner mCheckFrequencyView;
|
|
||||||
|
|
||||||
private Spinner mDisplayCountView;
|
|
||||||
|
|
||||||
private CheckBox mDefaultView;
|
|
||||||
|
|
||||||
private CheckBox mNotifyView;
|
|
||||||
private CheckBox mNotifyRingtoneView;
|
|
||||||
|
|
||||||
private Account mAccount;
|
|
||||||
|
|
||||||
public static void actionOptions(Context context, Account account, boolean makeDefault) {
|
|
||||||
Intent i = new Intent(context, AccountSetupOptions.class);
|
|
||||||
i.putExtra(EXTRA_ACCOUNT, account);
|
|
||||||
i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault);
|
|
||||||
context.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.account_setup_options);
|
|
||||||
|
|
||||||
mCheckFrequencyView = (Spinner)findViewById(R.id.account_check_frequency);
|
|
||||||
mDisplayCountView = (Spinner)findViewById(R.id.account_display_count);
|
|
||||||
mDefaultView = (CheckBox)findViewById(R.id.account_default);
|
|
||||||
mNotifyView = (CheckBox)findViewById(R.id.account_notify);
|
|
||||||
mNotifyRingtoneView = (CheckBox)findViewById(R.id.account_notify_ringtone);
|
|
||||||
|
|
||||||
findViewById(R.id.next).setOnClickListener(this);
|
|
||||||
|
|
||||||
SpinnerOption checkFrequencies[] = {
|
|
||||||
new SpinnerOption(-1,
|
|
||||||
getString(R.string.account_setup_options_mail_check_frequency_never)),
|
|
||||||
new SpinnerOption(5,
|
|
||||||
getString(R.string.account_setup_options_mail_check_frequency_5min)),
|
|
||||||
new SpinnerOption(10,
|
|
||||||
getString(R.string.account_setup_options_mail_check_frequency_10min)),
|
|
||||||
new SpinnerOption(15,
|
|
||||||
getString(R.string.account_setup_options_mail_check_frequency_15min)),
|
|
||||||
new SpinnerOption(30,
|
|
||||||
getString(R.string.account_setup_options_mail_check_frequency_30min)),
|
|
||||||
new SpinnerOption(60,
|
|
||||||
getString(R.string.account_setup_options_mail_check_frequency_1hour)),
|
|
||||||
};
|
|
||||||
|
|
||||||
ArrayAdapter<SpinnerOption> checkFrequenciesAdapter = new ArrayAdapter<SpinnerOption>(this,
|
|
||||||
android.R.layout.simple_spinner_item, checkFrequencies);
|
|
||||||
checkFrequenciesAdapter
|
|
||||||
.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
mCheckFrequencyView.setAdapter(checkFrequenciesAdapter);
|
|
||||||
|
|
||||||
SpinnerOption displayCounts[] = {
|
|
||||||
new SpinnerOption(10,
|
|
||||||
getString(R.string.account_setup_options_mail_display_count_10)),
|
|
||||||
new SpinnerOption(25,
|
|
||||||
getString(R.string.account_setup_options_mail_display_count_25)),
|
|
||||||
new SpinnerOption(50,
|
|
||||||
getString(R.string.account_setup_options_mail_display_count_50)),
|
|
||||||
new SpinnerOption(100,
|
|
||||||
getString(R.string.account_setup_options_mail_display_count_100)),
|
|
||||||
};
|
|
||||||
|
|
||||||
ArrayAdapter<SpinnerOption> displayCountsAdapter = new ArrayAdapter<SpinnerOption>(this,
|
|
||||||
android.R.layout.simple_spinner_item, displayCounts);
|
|
||||||
displayCountsAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
mDisplayCountView.setAdapter(displayCountsAdapter);
|
|
||||||
|
|
||||||
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
|
|
||||||
boolean makeDefault = getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false);
|
|
||||||
|
|
||||||
if (mAccount.equals(Preferences.getPreferences(this).getDefaultAccount()) || makeDefault) {
|
|
||||||
mDefaultView.setChecked(true);
|
|
||||||
}
|
|
||||||
mNotifyView.setChecked(mAccount.isNotifyNewMail());
|
|
||||||
mNotifyRingtoneView.setChecked(mAccount.isNotifyRingtone());
|
|
||||||
SpinnerOption.setSpinnerOptionValue(mCheckFrequencyView, mAccount
|
|
||||||
.getAutomaticCheckIntervalMinutes());
|
|
||||||
SpinnerOption.setSpinnerOptionValue(mDisplayCountView, mAccount
|
|
||||||
.getDisplayCount());
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onDone() {
|
|
||||||
mAccount.setDescription(mAccount.getEmail());
|
|
||||||
mAccount.setNotifyNewMail(mNotifyView.isChecked());
|
|
||||||
mAccount.setNotifyRingtone(mNotifyRingtoneView.isChecked());
|
|
||||||
mAccount.setAutomaticCheckIntervalMinutes((Integer)((SpinnerOption)mCheckFrequencyView
|
|
||||||
.getSelectedItem()).value);
|
|
||||||
mAccount.setDisplayCount((Integer)((SpinnerOption)mDisplayCountView
|
|
||||||
.getSelectedItem()).value);
|
|
||||||
mAccount.save(Preferences.getPreferences(this));
|
|
||||||
if (mDefaultView.isChecked()) {
|
|
||||||
Preferences.getPreferences(this).setDefaultAccount(mAccount);
|
|
||||||
}
|
|
||||||
k9.setServicesEnabled(this);
|
|
||||||
AccountSetupNames.actionSetNames(this, mAccount);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(View v) {
|
|
||||||
switch (v.getId()) {
|
|
||||||
case R.id.next:
|
|
||||||
onDone();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,285 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.activity.setup;
|
|
||||||
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.Editable;
|
|
||||||
import android.text.TextWatcher;
|
|
||||||
import android.text.method.DigitsKeyListener;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.View.OnClickListener;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.Button;
|
|
||||||
import android.widget.CheckBox;
|
|
||||||
import android.widget.CompoundButton;
|
|
||||||
import android.widget.EditText;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import android.widget.CompoundButton.OnCheckedChangeListener;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.Preferences;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
import com.fsck.k9.Utility;
|
|
||||||
|
|
||||||
public class AccountSetupOutgoing extends Activity implements OnClickListener,
|
|
||||||
OnCheckedChangeListener {
|
|
||||||
private static final String EXTRA_ACCOUNT = "account";
|
|
||||||
|
|
||||||
private static final String EXTRA_MAKE_DEFAULT = "makeDefault";
|
|
||||||
|
|
||||||
private static final int smtpPorts[] = {
|
|
||||||
25, 465, 465, 25, 25
|
|
||||||
};
|
|
||||||
|
|
||||||
private static final String smtpSchemes[] = {
|
|
||||||
"smtp", "smtp+ssl", "smtp+ssl+", "smtp+tls", "smtp+tls+"
|
|
||||||
};
|
|
||||||
private static final int webdavPorts[] = {
|
|
||||||
80, 443, 443, 443, 443
|
|
||||||
};
|
|
||||||
private static final String webdavSchemes[] = {
|
|
||||||
"webdav", "webdav+ssl", "webdav+ssl+", "webdav+tls", "webdav+tls+"
|
|
||||||
};
|
|
||||||
|
|
||||||
private EditText mUsernameView;
|
|
||||||
private EditText mPasswordView;
|
|
||||||
private EditText mServerView;
|
|
||||||
private EditText mPortView;
|
|
||||||
private CheckBox mRequireLoginView;
|
|
||||||
private ViewGroup mRequireLoginSettingsView;
|
|
||||||
private Spinner mSecurityTypeView;
|
|
||||||
private Button mNextButton;
|
|
||||||
private Account mAccount;
|
|
||||||
private boolean mMakeDefault;
|
|
||||||
|
|
||||||
public static void actionOutgoingSettings(Context context, Account account, boolean makeDefault) {
|
|
||||||
Intent i = new Intent(context, AccountSetupOutgoing.class);
|
|
||||||
i.putExtra(EXTRA_ACCOUNT, account);
|
|
||||||
i.putExtra(EXTRA_MAKE_DEFAULT, makeDefault);
|
|
||||||
context.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void actionEditOutgoingSettings(Context context, Account account) {
|
|
||||||
Intent i = new Intent(context, AccountSetupOutgoing.class);
|
|
||||||
i.setAction(Intent.ACTION_EDIT);
|
|
||||||
i.putExtra(EXTRA_ACCOUNT, account);
|
|
||||||
context.startActivity(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.account_setup_outgoing);
|
|
||||||
|
|
||||||
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (new URI(mAccount.getStoreUri()).getScheme().startsWith("webdav")) {
|
|
||||||
mAccount.setTransportUri(mAccount.getStoreUri());
|
|
||||||
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, false, true);
|
|
||||||
}
|
|
||||||
} catch (URISyntaxException e) {
|
|
||||||
// TODO Auto-generated catch block
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
mUsernameView = (EditText)findViewById(R.id.account_username);
|
|
||||||
mPasswordView = (EditText)findViewById(R.id.account_password);
|
|
||||||
mServerView = (EditText)findViewById(R.id.account_server);
|
|
||||||
mPortView = (EditText)findViewById(R.id.account_port);
|
|
||||||
mRequireLoginView = (CheckBox)findViewById(R.id.account_require_login);
|
|
||||||
mRequireLoginSettingsView = (ViewGroup)findViewById(R.id.account_require_login_settings);
|
|
||||||
mSecurityTypeView = (Spinner)findViewById(R.id.account_security_type);
|
|
||||||
mNextButton = (Button)findViewById(R.id.next);
|
|
||||||
|
|
||||||
mNextButton.setOnClickListener(this);
|
|
||||||
mRequireLoginView.setOnCheckedChangeListener(this);
|
|
||||||
|
|
||||||
SpinnerOption securityTypes[] = {
|
|
||||||
new SpinnerOption(0, getString(R.string.account_setup_incoming_security_none_label)),
|
|
||||||
new SpinnerOption(1,
|
|
||||||
getString(R.string.account_setup_incoming_security_ssl_optional_label)),
|
|
||||||
new SpinnerOption(2, getString(R.string.account_setup_incoming_security_ssl_label)),
|
|
||||||
new SpinnerOption(3,
|
|
||||||
getString(R.string.account_setup_incoming_security_tls_optional_label)),
|
|
||||||
new SpinnerOption(4, getString(R.string.account_setup_incoming_security_tls_label)),
|
|
||||||
};
|
|
||||||
|
|
||||||
ArrayAdapter<SpinnerOption> securityTypesAdapter = new ArrayAdapter<SpinnerOption>(this,
|
|
||||||
android.R.layout.simple_spinner_item, securityTypes);
|
|
||||||
securityTypesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
mSecurityTypeView.setAdapter(securityTypesAdapter);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Updates the port when the user changes the security type. This allows
|
|
||||||
* us to show a reasonable default which the user can change.
|
|
||||||
*/
|
|
||||||
mSecurityTypeView.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
|
||||||
public void onItemSelected(AdapterView arg0, View arg1, int arg2, long arg3) {
|
|
||||||
updatePortFromSecurityType();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onNothingSelected(AdapterView<?> arg0) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Calls validateFields() which enables or disables the Next button
|
|
||||||
* based on the fields' validity.
|
|
||||||
*/
|
|
||||||
TextWatcher validationTextWatcher = new TextWatcher() {
|
|
||||||
public void afterTextChanged(Editable s) {
|
|
||||||
validateFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
|
||||||
}
|
|
||||||
};
|
|
||||||
mUsernameView.addTextChangedListener(validationTextWatcher);
|
|
||||||
mPasswordView.addTextChangedListener(validationTextWatcher);
|
|
||||||
mServerView.addTextChangedListener(validationTextWatcher);
|
|
||||||
mPortView.addTextChangedListener(validationTextWatcher);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Only allow digits in the port field.
|
|
||||||
*/
|
|
||||||
mPortView.setKeyListener(DigitsKeyListener.getInstance("0123456789"));
|
|
||||||
|
|
||||||
mAccount = (Account)getIntent().getSerializableExtra(EXTRA_ACCOUNT);
|
|
||||||
mMakeDefault = (boolean)getIntent().getBooleanExtra(EXTRA_MAKE_DEFAULT, false);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If we're being reloaded we override the original account with the one
|
|
||||||
* we saved
|
|
||||||
*/
|
|
||||||
if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_ACCOUNT)) {
|
|
||||||
mAccount = (Account)savedInstanceState.getSerializable(EXTRA_ACCOUNT);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
URI uri = new URI(mAccount.getTransportUri());
|
|
||||||
String username = null;
|
|
||||||
String password = null;
|
|
||||||
if (uri.getUserInfo() != null) {
|
|
||||||
String[] userInfoParts = uri.getUserInfo().split(":", 2);
|
|
||||||
username = userInfoParts[0];
|
|
||||||
if (userInfoParts.length > 1) {
|
|
||||||
password = userInfoParts[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (username != null) {
|
|
||||||
mUsernameView.setText(username);
|
|
||||||
mRequireLoginView.setChecked(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (password != null) {
|
|
||||||
mPasswordView.setText(password);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0; i < smtpSchemes.length; i++) {
|
|
||||||
if (smtpSchemes[i].equals(uri.getScheme())) {
|
|
||||||
SpinnerOption.setSpinnerOptionValue(mSecurityTypeView, i);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri.getHost() != null) {
|
|
||||||
mServerView.setText(uri.getHost());
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri.getPort() != -1) {
|
|
||||||
mPortView.setText(Integer.toString(uri.getPort()));
|
|
||||||
} else {
|
|
||||||
updatePortFromSecurityType();
|
|
||||||
}
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
/*
|
|
||||||
* We should always be able to parse our own settings.
|
|
||||||
*/
|
|
||||||
throw new Error(use);
|
|
||||||
}
|
|
||||||
|
|
||||||
validateFields();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
|
||||||
super.onSaveInstanceState(outState);
|
|
||||||
outState.putSerializable(EXTRA_ACCOUNT, mAccount);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void validateFields() {
|
|
||||||
mNextButton
|
|
||||||
.setEnabled(
|
|
||||||
Utility.requiredFieldValid(mServerView) &&
|
|
||||||
Utility.requiredFieldValid(mPortView) &&
|
|
||||||
(!mRequireLoginView.isChecked() ||
|
|
||||||
(Utility.requiredFieldValid(mUsernameView) &&
|
|
||||||
Utility.requiredFieldValid(mPasswordView))));
|
|
||||||
Utility.setCompoundDrawablesAlpha(mNextButton, mNextButton.isEnabled() ? 255 : 128);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updatePortFromSecurityType() {
|
|
||||||
int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
|
|
||||||
mPortView.setText(Integer.toString(smtpPorts[securityType]));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
|
||||||
if (resultCode == RESULT_OK) {
|
|
||||||
if (Intent.ACTION_EDIT.equals(getIntent().getAction())) {
|
|
||||||
mAccount.save(Preferences.getPreferences(this));
|
|
||||||
finish();
|
|
||||||
} else {
|
|
||||||
AccountSetupOptions.actionOptions(this, mAccount, mMakeDefault);
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onNext() {
|
|
||||||
int securityType = (Integer)((SpinnerOption)mSecurityTypeView.getSelectedItem()).value;
|
|
||||||
URI uri;
|
|
||||||
try {
|
|
||||||
String userInfo = null;
|
|
||||||
if (mRequireLoginView.isChecked()) {
|
|
||||||
userInfo = mUsernameView.getText().toString() + ":"
|
|
||||||
+ mPasswordView.getText().toString();
|
|
||||||
}
|
|
||||||
uri = new URI(smtpSchemes[securityType], userInfo, mServerView.getText().toString(),
|
|
||||||
Integer.parseInt(mPortView.getText().toString()), null, null, null);
|
|
||||||
mAccount.setTransportUri(uri.toString());
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
/*
|
|
||||||
* It's unrecoverable if we cannot create a URI from components that
|
|
||||||
* we validated to be safe.
|
|
||||||
*/
|
|
||||||
throw new Error(use);
|
|
||||||
}
|
|
||||||
AccountSetupCheckSettings.actionCheckSettings(this, mAccount, false, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onClick(View v) {
|
|
||||||
switch (v.getId()) {
|
|
||||||
case R.id.next:
|
|
||||||
onNext();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
|
|
||||||
mRequireLoginSettingsView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
|
|
||||||
validateFields();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.fsck.k9.activity.setup;
|
|
||||||
|
|
||||||
import android.widget.Spinner;
|
|
||||||
|
|
||||||
public class SpinnerOption {
|
|
||||||
public Object value;
|
|
||||||
|
|
||||||
public String label;
|
|
||||||
|
|
||||||
public static void setSpinnerOptionValue(Spinner spinner, Object value) {
|
|
||||||
for (int i = 0, count = spinner.getCount(); i < count; i++) {
|
|
||||||
SpinnerOption so = (SpinnerOption)spinner.getItemAtPosition(i);
|
|
||||||
if (so.value.equals(value)) {
|
|
||||||
spinner.setSelection(i, true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public SpinnerOption(Object value, String label) {
|
|
||||||
this.value = value;
|
|
||||||
this.label = label;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,788 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You 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.fsck.k9.codec.binary;
|
|
||||||
|
|
||||||
import org.apache.commons.codec.BinaryDecoder;
|
|
||||||
import org.apache.commons.codec.BinaryEncoder;
|
|
||||||
import org.apache.commons.codec.DecoderException;
|
|
||||||
import org.apache.commons.codec.EncoderException;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.math.BigInteger;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides Base64 encoding and decoding as defined by RFC 2045.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
|
|
||||||
* Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
|
|
||||||
* @author Apache Software Foundation
|
|
||||||
* @since 1.0-dev
|
|
||||||
* @version $Id$
|
|
||||||
*/
|
|
||||||
public class Base64 implements BinaryEncoder, BinaryDecoder {
|
|
||||||
/**
|
|
||||||
* Chunk size per RFC 2045 section 6.8.
|
|
||||||
*
|
|
||||||
* <p>
|
|
||||||
* The {@value} character limit does not count the trailing CRLF, but counts all other characters, including any
|
|
||||||
* equal signs.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 6.8</a>
|
|
||||||
*/
|
|
||||||
static final int CHUNK_SIZE = 76;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Chunk separator per RFC 2045 section 2.1.
|
|
||||||
*
|
|
||||||
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045 section 2.1</a>
|
|
||||||
*/
|
|
||||||
static final byte[] CHUNK_SEPARATOR = {'\r','\n'};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This array is a lookup table that translates 6-bit positive integer
|
|
||||||
* index values into their "Base64 Alphabet" equivalents as specified
|
|
||||||
* in Table 1 of RFC 2045.
|
|
||||||
*
|
|
||||||
* Thanks to "commons" project in ws.apache.org for this code.
|
|
||||||
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
|
|
||||||
*/
|
|
||||||
private static final byte[] intToBase64 = {
|
|
||||||
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
|
|
||||||
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
|
||||||
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
|
|
||||||
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
|
||||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '+', '/'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Byte used to pad output.
|
|
||||||
*/
|
|
||||||
private static final byte PAD = '=';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This array is a lookup table that translates unicode characters
|
|
||||||
* drawn from the "Base64 Alphabet" (as specified in Table 1 of RFC 2045)
|
|
||||||
* into their 6-bit positive integer equivalents. Characters that
|
|
||||||
* are not in the Base64 alphabet but fall within the bounds of the
|
|
||||||
* array are translated to -1.
|
|
||||||
*
|
|
||||||
* Thanks to "commons" project in ws.apache.org for this code.
|
|
||||||
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
|
|
||||||
*/
|
|
||||||
private static final byte[] base64ToInt = {
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,
|
|
||||||
-1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, 52, 53, 54,
|
|
||||||
55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, -1, 0, 1, 2, 3, 4,
|
|
||||||
5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
|
|
||||||
24, 25, -1, -1, -1, -1, -1, -1, 26, 27, 28, 29, 30, 31, 32, 33, 34,
|
|
||||||
35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51
|
|
||||||
};
|
|
||||||
|
|
||||||
/** Mask used to extract 6 bits, used when encoding */
|
|
||||||
private static final int MASK_6BITS = 0x3f;
|
|
||||||
|
|
||||||
/** Mask used to extract 8 bits, used in decoding base64 bytes */
|
|
||||||
private static final int MASK_8BITS = 0xff;
|
|
||||||
|
|
||||||
// The static final fields above are used for the original static byte[] methods on Base64.
|
|
||||||
// The private member fields below are used with the new streaming approach, which requires
|
|
||||||
// some state be preserved between calls of encode() and decode().
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Line length for encoding. Not used when decoding. A value of zero or less implies
|
|
||||||
* no chunking of the base64 encoded data.
|
|
||||||
*/
|
|
||||||
private final int lineLength;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Line separator for encoding. Not used when decoding. Only used if lineLength > 0.
|
|
||||||
*/
|
|
||||||
private final byte[] lineSeparator;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience variable to help us determine when our buffer is going to run out of
|
|
||||||
* room and needs resizing. <code>decodeSize = 3 + lineSeparator.length;</code>
|
|
||||||
*/
|
|
||||||
private final int decodeSize;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convenience variable to help us determine when our buffer is going to run out of
|
|
||||||
* room and needs resizing. <code>encodeSize = 4 + lineSeparator.length;</code>
|
|
||||||
*/
|
|
||||||
private final int encodeSize;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Buffer for streaming.
|
|
||||||
*/
|
|
||||||
private byte[] buf;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Position where next character should be written in the buffer.
|
|
||||||
*/
|
|
||||||
private int pos;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Position where next character should be read from the buffer.
|
|
||||||
*/
|
|
||||||
private int readPos;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Variable tracks how many characters have been written to the current line.
|
|
||||||
* Only used when encoding. We use it to make sure each encoded line never
|
|
||||||
* goes beyond lineLength (if lineLength > 0).
|
|
||||||
*/
|
|
||||||
private int currentLinePos;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes to the buffer only occur after every 3 reads when encoding, an
|
|
||||||
* every 4 reads when decoding. This variable helps track that.
|
|
||||||
*/
|
|
||||||
private int modulus;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Boolean flag to indicate the EOF has been reached. Once EOF has been
|
|
||||||
* reached, this Base64 object becomes useless, and must be thrown away.
|
|
||||||
*/
|
|
||||||
private boolean eof;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Place holder for the 3 bytes we're dealing with for our base64 logic.
|
|
||||||
* Bitwise operations store and extract the base64 encoding or decoding from
|
|
||||||
* this variable.
|
|
||||||
*/
|
|
||||||
private int x;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default constructor: lineLength is 76, and the lineSeparator is CRLF
|
|
||||||
* when encoding, and all forms can be decoded.
|
|
||||||
*/
|
|
||||||
public Base64() {
|
|
||||||
this(CHUNK_SIZE, CHUNK_SEPARATOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>
|
|
||||||
* Consumer can use this constructor to choose a different lineLength
|
|
||||||
* when encoding (lineSeparator is still CRLF). All forms of data can
|
|
||||||
* be decoded.
|
|
||||||
* </p><p>
|
|
||||||
* Note: lineLengths that aren't multiples of 4 will still essentially
|
|
||||||
* end up being multiples of 4 in the encoded data.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param lineLength each line of encoded data will be at most this long
|
|
||||||
* (rounded up to nearest multiple of 4).
|
|
||||||
* If lineLength <= 0, then the output will not be divided into lines (chunks).
|
|
||||||
* Ignored when decoding.
|
|
||||||
*/
|
|
||||||
public Base64(int lineLength) {
|
|
||||||
this(lineLength, CHUNK_SEPARATOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>
|
|
||||||
* Consumer can use this constructor to choose a different lineLength
|
|
||||||
* and lineSeparator when encoding. All forms of data can
|
|
||||||
* be decoded.
|
|
||||||
* </p><p>
|
|
||||||
* Note: lineLengths that aren't multiples of 4 will still essentially
|
|
||||||
* end up being multiples of 4 in the encoded data.
|
|
||||||
* </p>
|
|
||||||
* @param lineLength Each line of encoded data will be at most this long
|
|
||||||
* (rounded up to nearest multiple of 4). Ignored when decoding.
|
|
||||||
* If <= 0, then output will not be divided into lines (chunks).
|
|
||||||
* @param lineSeparator Each line of encoded data will end with this
|
|
||||||
* sequence of bytes.
|
|
||||||
* If lineLength <= 0, then the lineSeparator is not used.
|
|
||||||
* @throws IllegalArgumentException The provided lineSeparator included
|
|
||||||
* some base64 characters. That's not going to work!
|
|
||||||
*/
|
|
||||||
public Base64(int lineLength, byte[] lineSeparator) {
|
|
||||||
this.lineLength = lineLength;
|
|
||||||
this.lineSeparator = new byte[lineSeparator.length];
|
|
||||||
System.arraycopy(lineSeparator, 0, this.lineSeparator, 0, lineSeparator.length);
|
|
||||||
if (lineLength > 0) {
|
|
||||||
this.encodeSize = 4 + lineSeparator.length;
|
|
||||||
} else {
|
|
||||||
this.encodeSize = 4;
|
|
||||||
}
|
|
||||||
this.decodeSize = encodeSize - 1;
|
|
||||||
if (containsBase64Byte(lineSeparator)) {
|
|
||||||
String sep;
|
|
||||||
try {
|
|
||||||
sep = new String(lineSeparator, "UTF-8");
|
|
||||||
} catch (UnsupportedEncodingException uee) {
|
|
||||||
sep = new String(lineSeparator);
|
|
||||||
}
|
|
||||||
throw new IllegalArgumentException("lineSeperator must not contain base64 characters: [" + sep + "]");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if this Base64 object has buffered data for reading.
|
|
||||||
*
|
|
||||||
* @return true if there is Base64 object still available for reading.
|
|
||||||
*/
|
|
||||||
boolean hasData() { return buf != null; }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the amount of buffered data available for reading.
|
|
||||||
*
|
|
||||||
* @return The amount of buffered data available for reading.
|
|
||||||
*/
|
|
||||||
int avail() { return buf != null ? pos - readPos : 0; }
|
|
||||||
|
|
||||||
/** Doubles our buffer. */
|
|
||||||
private void resizeBuf() {
|
|
||||||
if (buf == null) {
|
|
||||||
buf = new byte[8192];
|
|
||||||
pos = 0;
|
|
||||||
readPos = 0;
|
|
||||||
} else {
|
|
||||||
byte[] b = new byte[buf.length * 2];
|
|
||||||
System.arraycopy(buf, 0, b, 0, buf.length);
|
|
||||||
buf = b;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extracts buffered data into the provided byte[] array, starting
|
|
||||||
* at position bPos, up to a maximum of bAvail bytes. Returns how
|
|
||||||
* many bytes were actually extracted.
|
|
||||||
*
|
|
||||||
* @param b byte[] array to extract the buffered data into.
|
|
||||||
* @param bPos position in byte[] array to start extraction at.
|
|
||||||
* @param bAvail amount of bytes we're allowed to extract. We may extract
|
|
||||||
* fewer (if fewer are available).
|
|
||||||
* @return The number of bytes successfully extracted into the provided
|
|
||||||
* byte[] array.
|
|
||||||
*/
|
|
||||||
int readResults(byte[] b, int bPos, int bAvail) {
|
|
||||||
if (buf != null) {
|
|
||||||
int len = Math.min(avail(), bAvail);
|
|
||||||
if (buf != b) {
|
|
||||||
System.arraycopy(buf, readPos, b, bPos, len);
|
|
||||||
readPos += len;
|
|
||||||
if (readPos >= pos) {
|
|
||||||
buf = null;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Re-using the original consumer's output array is only
|
|
||||||
// allowed for one round.
|
|
||||||
buf = null;
|
|
||||||
}
|
|
||||||
return len;
|
|
||||||
} else {
|
|
||||||
return eof ? -1 : 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Small optimization where we try to buffer directly to the consumer's
|
|
||||||
* output array for one round (if consumer calls this method first!) instead
|
|
||||||
* of starting our own buffer.
|
|
||||||
*
|
|
||||||
* @param out byte[] array to buffer directly to.
|
|
||||||
* @param outPos Position to start buffering into.
|
|
||||||
* @param outAvail Amount of bytes available for direct buffering.
|
|
||||||
*/
|
|
||||||
void setInitialBuffer(byte[] out, int outPos, int outAvail) {
|
|
||||||
// We can re-use consumer's original output array under
|
|
||||||
// special circumstances, saving on some System.arraycopy().
|
|
||||||
if (out != null && out.length == outAvail) {
|
|
||||||
buf = out;
|
|
||||||
pos = outPos;
|
|
||||||
readPos = outPos;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>
|
|
||||||
* Encodes all of the provided data, starting at inPos, for inAvail bytes.
|
|
||||||
* Must be called at least twice: once with the data to encode, and once
|
|
||||||
* with inAvail set to "-1" to alert encoder that EOF has been reached,
|
|
||||||
* so flush last remaining bytes (if not multiple of 3).
|
|
||||||
* </p><p>
|
|
||||||
* Thanks to "commons" project in ws.apache.org for the bitwise operations,
|
|
||||||
* and general approach.
|
|
||||||
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param in byte[] array of binary data to base64 encode.
|
|
||||||
* @param inPos Position to start reading data from.
|
|
||||||
* @param inAvail Amount of bytes available from input for encoding.
|
|
||||||
*/
|
|
||||||
void encode(byte[] in, int inPos, int inAvail) {
|
|
||||||
if (eof) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// inAvail < 0 is how we're informed of EOF in the underlying data we're
|
|
||||||
// encoding.
|
|
||||||
if (inAvail < 0) {
|
|
||||||
eof = true;
|
|
||||||
if (buf == null || buf.length - pos < encodeSize) {
|
|
||||||
resizeBuf();
|
|
||||||
}
|
|
||||||
switch (modulus) {
|
|
||||||
case 1:
|
|
||||||
buf[pos++] = intToBase64[(x >> 2) & MASK_6BITS];
|
|
||||||
buf[pos++] = intToBase64[(x << 4) & MASK_6BITS];
|
|
||||||
buf[pos++] = PAD;
|
|
||||||
buf[pos++] = PAD;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
buf[pos++] = intToBase64[(x >> 10) & MASK_6BITS];
|
|
||||||
buf[pos++] = intToBase64[(x >> 4) & MASK_6BITS];
|
|
||||||
buf[pos++] = intToBase64[(x << 2) & MASK_6BITS];
|
|
||||||
buf[pos++] = PAD;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (lineLength > 0) {
|
|
||||||
System.arraycopy(lineSeparator, 0, buf, pos, lineSeparator.length);
|
|
||||||
pos += lineSeparator.length;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (int i = 0; i < inAvail; i++) {
|
|
||||||
if (buf == null || buf.length - pos < encodeSize) {
|
|
||||||
resizeBuf();
|
|
||||||
}
|
|
||||||
modulus = (++modulus) % 3;
|
|
||||||
int b = in[inPos++];
|
|
||||||
if (b < 0) { b += 256; }
|
|
||||||
x = (x << 8) + b;
|
|
||||||
if (0 == modulus) {
|
|
||||||
buf[pos++] = intToBase64[(x >> 18) & MASK_6BITS];
|
|
||||||
buf[pos++] = intToBase64[(x >> 12) & MASK_6BITS];
|
|
||||||
buf[pos++] = intToBase64[(x >> 6) & MASK_6BITS];
|
|
||||||
buf[pos++] = intToBase64[x & MASK_6BITS];
|
|
||||||
currentLinePos += 4;
|
|
||||||
if (lineLength > 0 && lineLength <= currentLinePos) {
|
|
||||||
System.arraycopy(lineSeparator, 0, buf, pos, lineSeparator.length);
|
|
||||||
pos += lineSeparator.length;
|
|
||||||
currentLinePos = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>
|
|
||||||
* Decodes all of the provided data, starting at inPos, for inAvail bytes.
|
|
||||||
* Should be called at least twice: once with the data to decode, and once
|
|
||||||
* with inAvail set to "-1" to alert decoder that EOF has been reached.
|
|
||||||
* The "-1" call is not necessary when decoding, but it doesn't hurt, either.
|
|
||||||
* </p><p>
|
|
||||||
* Ignores all non-base64 characters. This is how chunked (e.g. 76 character)
|
|
||||||
* data is handled, since CR and LF are silently ignored, but has implications
|
|
||||||
* for other bytes, too. This method subscribes to the garbage-in, garbage-out
|
|
||||||
* philosophy: it will not check the provided data for validity.
|
|
||||||
* </p><p>
|
|
||||||
* Thanks to "commons" project in ws.apache.org for the bitwise operations,
|
|
||||||
* and general approach.
|
|
||||||
* http://svn.apache.org/repos/asf/webservices/commons/trunk/modules/util/
|
|
||||||
* </p>
|
|
||||||
|
|
||||||
* @param in byte[] array of ascii data to base64 decode.
|
|
||||||
* @param inPos Position to start reading data from.
|
|
||||||
* @param inAvail Amount of bytes available from input for encoding.
|
|
||||||
*/
|
|
||||||
void decode(byte[] in, int inPos, int inAvail) {
|
|
||||||
if (eof) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (inAvail < 0) {
|
|
||||||
eof = true;
|
|
||||||
}
|
|
||||||
for (int i = 0; i < inAvail; i++) {
|
|
||||||
if (buf == null || buf.length - pos < decodeSize) {
|
|
||||||
resizeBuf();
|
|
||||||
}
|
|
||||||
byte b = in[inPos++];
|
|
||||||
if (b == PAD) {
|
|
||||||
x = x << 6;
|
|
||||||
switch (modulus) {
|
|
||||||
case 2:
|
|
||||||
x = x << 6;
|
|
||||||
buf[pos++] = (byte) ((x >> 16) & MASK_8BITS);
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
buf[pos++] = (byte) ((x >> 16) & MASK_8BITS);
|
|
||||||
buf[pos++] = (byte) ((x >> 8) & MASK_8BITS);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// WE'RE DONE!!!!
|
|
||||||
eof = true;
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
if (b >= 0 && b < base64ToInt.length) {
|
|
||||||
int result = base64ToInt[b];
|
|
||||||
if (result >= 0) {
|
|
||||||
modulus = (++modulus) % 4;
|
|
||||||
x = (x << 6) + result;
|
|
||||||
if (modulus == 0) {
|
|
||||||
buf[pos++] = (byte) ((x >> 16) & MASK_8BITS);
|
|
||||||
buf[pos++] = (byte) ((x >> 8) & MASK_8BITS);
|
|
||||||
buf[pos++] = (byte) (x & MASK_8BITS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether or not the <code>octet</code> is in the base 64 alphabet.
|
|
||||||
*
|
|
||||||
* @param octet
|
|
||||||
* The value to test
|
|
||||||
* @return <code>true</code> if the value is defined in the the base 64 alphabet, <code>false</code> otherwise.
|
|
||||||
*/
|
|
||||||
public static boolean isBase64(byte octet) {
|
|
||||||
return octet == PAD || (octet >= 0 && octet < base64ToInt.length && base64ToInt[octet] != -1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests a given byte array to see if it contains only valid characters within the Base64 alphabet.
|
|
||||||
* Currently the method treats whitespace as valid.
|
|
||||||
*
|
|
||||||
* @param arrayOctet
|
|
||||||
* byte array to test
|
|
||||||
* @return <code>true</code> if all bytes are valid characters in the Base64 alphabet or if the byte array is
|
|
||||||
* empty; false, otherwise
|
|
||||||
*/
|
|
||||||
public static boolean isArrayByteBase64(byte[] arrayOctet) {
|
|
||||||
for (int i = 0; i < arrayOctet.length; i++) {
|
|
||||||
if (!isBase64(arrayOctet[i]) && !isWhiteSpace(arrayOctet[i])) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Tests a given byte array to see if it contains only valid characters within the Base64 alphabet.
|
|
||||||
*
|
|
||||||
* @param arrayOctet
|
|
||||||
* byte array to test
|
|
||||||
* @return <code>true</code> if any byte is a valid character in the Base64 alphabet; false herwise
|
|
||||||
*/
|
|
||||||
private static boolean containsBase64Byte(byte[] arrayOctet) {
|
|
||||||
for (int i = 0; i < arrayOctet.length; i++) {
|
|
||||||
if (isBase64(arrayOctet[i])) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes binary data using the base64 algorithm but does not chunk the output.
|
|
||||||
*
|
|
||||||
* @param binaryData
|
|
||||||
* binary data to encode
|
|
||||||
* @return Base64 characters
|
|
||||||
*/
|
|
||||||
public static byte[] encodeBase64(byte[] binaryData) {
|
|
||||||
return encodeBase64(binaryData, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes binary data using the base64 algorithm and chunks the encoded output into 76 character blocks
|
|
||||||
*
|
|
||||||
* @param binaryData
|
|
||||||
* binary data to encode
|
|
||||||
* @return Base64 characters chunked in 76 character blocks
|
|
||||||
*/
|
|
||||||
public static byte[] encodeBase64Chunked(byte[] binaryData) {
|
|
||||||
return encodeBase64(binaryData, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes an Object using the base64 algorithm. This method is provided in order to satisfy the requirements of the
|
|
||||||
* Decoder interface, and will throw a DecoderException if the supplied object is not of type byte[].
|
|
||||||
*
|
|
||||||
* @param pObject
|
|
||||||
* Object to decode
|
|
||||||
* @return An object (of type byte[]) containing the binary data which corresponds to the byte[] supplied.
|
|
||||||
* @throws DecoderException
|
|
||||||
* if the parameter supplied is not of type byte[]
|
|
||||||
*/
|
|
||||||
public Object decode(Object pObject) throws DecoderException {
|
|
||||||
if (!(pObject instanceof byte[])) {
|
|
||||||
throw new DecoderException("Parameter supplied to Base64 decode is not a byte[]");
|
|
||||||
}
|
|
||||||
return decode((byte[]) pObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes a byte[] containing containing characters in the Base64 alphabet.
|
|
||||||
*
|
|
||||||
* @param pArray
|
|
||||||
* A byte array containing Base64 character data
|
|
||||||
* @return a byte array containing binary data
|
|
||||||
*/
|
|
||||||
public byte[] decode(byte[] pArray) {
|
|
||||||
return decodeBase64(pArray);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes binary data using the base64 algorithm, optionally chunking the output into 76 character blocks.
|
|
||||||
*
|
|
||||||
* @param binaryData
|
|
||||||
* Array containing binary data to encode.
|
|
||||||
* @param isChunked
|
|
||||||
* if <code>true</code> this encoder will chunk the base64 output into 76 character blocks
|
|
||||||
* @return Base64-encoded data.
|
|
||||||
* @throws IllegalArgumentException
|
|
||||||
* Thrown when the input array needs an output array bigger than {@link Integer#MAX_VALUE}
|
|
||||||
*/
|
|
||||||
public static byte[] encodeBase64(byte[] binaryData, boolean isChunked) {
|
|
||||||
if (binaryData == null || binaryData.length == 0) {
|
|
||||||
return binaryData;
|
|
||||||
}
|
|
||||||
Base64 b64 = isChunked ? new Base64() : new Base64(0);
|
|
||||||
|
|
||||||
long len = (binaryData.length * 4) / 3;
|
|
||||||
long mod = len % 4;
|
|
||||||
if (mod != 0) {
|
|
||||||
len += 4 - mod;
|
|
||||||
}
|
|
||||||
if (isChunked) {
|
|
||||||
len += (1 + (len / CHUNK_SIZE)) * CHUNK_SEPARATOR.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (len > Integer.MAX_VALUE) {
|
|
||||||
throw new IllegalArgumentException(
|
|
||||||
"Input array too big, output array would be bigger than Integer.MAX_VALUE=" + Integer.MAX_VALUE);
|
|
||||||
}
|
|
||||||
byte[] buf = new byte[(int) len];
|
|
||||||
b64.setInitialBuffer(buf, 0, buf.length);
|
|
||||||
b64.encode(binaryData, 0, binaryData.length);
|
|
||||||
b64.encode(binaryData, 0, -1); // Notify encoder of EOF.
|
|
||||||
|
|
||||||
// Encoder might have resized, even though it was unnecessary.
|
|
||||||
if (b64.buf != buf) {
|
|
||||||
b64.readResults(buf, 0, buf.length);
|
|
||||||
}
|
|
||||||
return buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decodes Base64 data into octets
|
|
||||||
*
|
|
||||||
* @param base64Data Byte array containing Base64 data
|
|
||||||
* @return Array containing decoded data.
|
|
||||||
*/
|
|
||||||
public static byte[] decodeBase64(byte[] base64Data) {
|
|
||||||
if (base64Data == null || base64Data.length == 0) {
|
|
||||||
return base64Data;
|
|
||||||
}
|
|
||||||
Base64 b64 = new Base64();
|
|
||||||
|
|
||||||
long len = (base64Data.length * 3) / 4;
|
|
||||||
byte[] buf = new byte[(int) len];
|
|
||||||
b64.setInitialBuffer(buf, 0, buf.length);
|
|
||||||
b64.decode(base64Data, 0, base64Data.length);
|
|
||||||
b64.decode(base64Data, 0, -1); // Notify decoder of EOF.
|
|
||||||
|
|
||||||
// We have no idea what the line-length was, so we
|
|
||||||
// cannot know how much of our array wasn't used.
|
|
||||||
byte[] result = new byte[b64.pos];
|
|
||||||
b64.readResults(result, 0, result.length);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discards any whitespace from a base-64 encoded block.
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* The base-64 encoded data to discard the whitespace from.
|
|
||||||
* @return The data, less whitespace (see RFC 2045).
|
|
||||||
* @deprecated This method is no longer needed
|
|
||||||
*/
|
|
||||||
static byte[] discardWhitespace(byte[] data) {
|
|
||||||
byte groomedData[] = new byte[data.length];
|
|
||||||
int bytesCopied = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < data.length; i++) {
|
|
||||||
switch (data[i]) {
|
|
||||||
case ' ' :
|
|
||||||
case '\n' :
|
|
||||||
case '\r' :
|
|
||||||
case '\t' :
|
|
||||||
break;
|
|
||||||
default :
|
|
||||||
groomedData[bytesCopied++] = data[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
byte packedData[] = new byte[bytesCopied];
|
|
||||||
|
|
||||||
System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
|
|
||||||
|
|
||||||
return packedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a byte value is whitespace or not.
|
|
||||||
*
|
|
||||||
* @param byteToCheck the byte to check
|
|
||||||
* @return true if byte is whitespace, false otherwise
|
|
||||||
*/
|
|
||||||
private static boolean isWhiteSpace(byte byteToCheck){
|
|
||||||
switch (byteToCheck) {
|
|
||||||
case ' ' :
|
|
||||||
case '\n' :
|
|
||||||
case '\r' :
|
|
||||||
case '\t' :
|
|
||||||
return true;
|
|
||||||
default :
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Discards any characters outside of the base64 alphabet, per the requirements on page 25 of RFC 2045 - "Any
|
|
||||||
* characters outside of the base64 alphabet are to be ignored in base64 encoded data."
|
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
* The base-64 encoded data to groom
|
|
||||||
* @return The data, less non-base64 characters (see RFC 2045).
|
|
||||||
*/
|
|
||||||
static byte[] discardNonBase64(byte[] data) {
|
|
||||||
byte groomedData[] = new byte[data.length];
|
|
||||||
int bytesCopied = 0;
|
|
||||||
|
|
||||||
for (int i = 0; i < data.length; i++) {
|
|
||||||
if (isBase64(data[i])) {
|
|
||||||
groomedData[bytesCopied++] = data[i];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
byte packedData[] = new byte[bytesCopied];
|
|
||||||
|
|
||||||
System.arraycopy(groomedData, 0, packedData, 0, bytesCopied);
|
|
||||||
|
|
||||||
return packedData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation of the Encoder Interface
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes an Object using the base64 algorithm. This method is provided in order to satisfy the requirements of the
|
|
||||||
* Encoder interface, and will throw an EncoderException if the supplied object is not of type byte[].
|
|
||||||
*
|
|
||||||
* @param pObject
|
|
||||||
* Object to encode
|
|
||||||
* @return An object (of type byte[]) containing the base64 encoded data which corresponds to the byte[] supplied.
|
|
||||||
* @throws EncoderException
|
|
||||||
* if the parameter supplied is not of type byte[]
|
|
||||||
*/
|
|
||||||
public Object encode(Object pObject) throws EncoderException {
|
|
||||||
if (!(pObject instanceof byte[])) {
|
|
||||||
throw new EncoderException("Parameter supplied to Base64 encode is not a byte[]");
|
|
||||||
}
|
|
||||||
return encode((byte[]) pObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encodes a byte[] containing binary data, into a byte[] containing characters in the Base64 alphabet.
|
|
||||||
*
|
|
||||||
* @param pArray
|
|
||||||
* a byte array containing binary data
|
|
||||||
* @return A byte array containing only Base64 character data
|
|
||||||
*/
|
|
||||||
public byte[] encode(byte[] pArray) {
|
|
||||||
return encodeBase64(pArray, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implementation of integer encoding used for crypto
|
|
||||||
/**
|
|
||||||
* Decode a byte64-encoded integer according to crypto
|
|
||||||
* standards such as W3C's XML-Signature
|
|
||||||
*
|
|
||||||
* @param pArray a byte array containing base64 character data
|
|
||||||
* @return A BigInteger
|
|
||||||
*/
|
|
||||||
public static BigInteger decodeInteger(byte[] pArray) {
|
|
||||||
return new BigInteger(1, decodeBase64(pArray));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encode to a byte64-encoded integer according to crypto
|
|
||||||
* standards such as W3C's XML-Signature
|
|
||||||
*
|
|
||||||
* @param bigInt a BigInteger
|
|
||||||
* @return A byte array containing base64 character data
|
|
||||||
* @throws NullPointerException if null is passed in
|
|
||||||
*/
|
|
||||||
public static byte[] encodeInteger(BigInteger bigInt) {
|
|
||||||
if(bigInt == null) {
|
|
||||||
throw new NullPointerException("encodeInteger called with null parameter");
|
|
||||||
}
|
|
||||||
|
|
||||||
return encodeBase64(toIntegerBytes(bigInt), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a byte-array representation of a <code>BigInteger</code>
|
|
||||||
* without sign bit.
|
|
||||||
*
|
|
||||||
* @param bigInt <code>BigInteger</code> to be converted
|
|
||||||
* @return a byte array representation of the BigInteger parameter
|
|
||||||
*/
|
|
||||||
static byte[] toIntegerBytes(BigInteger bigInt) {
|
|
||||||
int bitlen = bigInt.bitLength();
|
|
||||||
// round bitlen
|
|
||||||
bitlen = ((bitlen + 7) >> 3) << 3;
|
|
||||||
byte[] bigBytes = bigInt.toByteArray();
|
|
||||||
|
|
||||||
if(((bigInt.bitLength() % 8) != 0) &&
|
|
||||||
(((bigInt.bitLength() / 8) + 1) == (bitlen / 8))) {
|
|
||||||
return bigBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up params for copying everything but sign bit
|
|
||||||
int startSrc = 0;
|
|
||||||
int len = bigBytes.length;
|
|
||||||
|
|
||||||
// if bigInt is exactly byte-aligned, just skip signbit in copy
|
|
||||||
if((bigInt.bitLength() % 8) == 0) {
|
|
||||||
startSrc = 1;
|
|
||||||
len--;
|
|
||||||
}
|
|
||||||
|
|
||||||
int startDst = bitlen / 8 - len; // to pad w/ nulls as per spec
|
|
||||||
byte[] resizedBytes = new byte[bitlen / 8];
|
|
||||||
|
|
||||||
System.arraycopy(bigBytes, startSrc, resizedBytes, startDst, len);
|
|
||||||
|
|
||||||
return resizedBytes;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,179 +0,0 @@
|
|||||||
/*
|
|
||||||
* Licensed to the Apache Software Foundation (ASF) under one or more
|
|
||||||
* contributor license agreements. See the NOTICE file distributed with
|
|
||||||
* this work for additional information regarding copyright ownership.
|
|
||||||
* The ASF licenses this file to You 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.fsck.k9.codec.binary;
|
|
||||||
|
|
||||||
import java.io.FilterOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides Base64 encoding and decoding in a streaming fashion (unlimited size).
|
|
||||||
* When encoding the default lineLength is 76 characters and the default
|
|
||||||
* lineEnding is CRLF, but these can be overridden by using the appropriate
|
|
||||||
* constructor.
|
|
||||||
* <p>
|
|
||||||
* The default behaviour of the Base64OutputStream is to ENCODE, whereas the
|
|
||||||
* default behaviour of the Base64InputStream is to DECODE. But this behaviour
|
|
||||||
* can be overridden by using a different constructor.
|
|
||||||
* </p><p>
|
|
||||||
* This class implements section <cite>6.8. Base64 Content-Transfer-Encoding</cite> from RFC 2045 <cite>Multipurpose
|
|
||||||
* Internet Mail Extensions (MIME) Part One: Format of Internet Message Bodies</cite> by Freed and Borenstein.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @author Apache Software Foundation
|
|
||||||
* @version $Id $
|
|
||||||
* @see <a href="http://www.ietf.org/rfc/rfc2045.txt">RFC 2045</a>
|
|
||||||
* @since 1.0-dev
|
|
||||||
*/
|
|
||||||
public class Base64OutputStream extends FilterOutputStream {
|
|
||||||
private final boolean doEncode;
|
|
||||||
private final Base64 base64;
|
|
||||||
private final byte[] singleByte = new byte[1];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Base64OutputStream such that all data written is Base64-encoded
|
|
||||||
* to the original provided OutputStream.
|
|
||||||
*
|
|
||||||
* @param out OutputStream to wrap.
|
|
||||||
*/
|
|
||||||
public Base64OutputStream(OutputStream out) {
|
|
||||||
this(out, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Base64OutputStream such that all data written is either
|
|
||||||
* Base64-encoded or Base64-decoded to the original provided OutputStream.
|
|
||||||
*
|
|
||||||
* @param out OutputStream to wrap.
|
|
||||||
* @param doEncode true if we should encode all data written to us,
|
|
||||||
* false if we should decode.
|
|
||||||
*/
|
|
||||||
public Base64OutputStream(OutputStream out, boolean doEncode) {
|
|
||||||
super(out);
|
|
||||||
this.doEncode = doEncode;
|
|
||||||
this.base64 = new Base64();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a Base64OutputStream such that all data written is either
|
|
||||||
* Base64-encoded or Base64-decoded to the original provided OutputStream.
|
|
||||||
*
|
|
||||||
* @param out OutputStream to wrap.
|
|
||||||
* @param doEncode true if we should encode all data written to us,
|
|
||||||
* false if we should decode.
|
|
||||||
* @param lineLength If doEncode is true, each line of encoded
|
|
||||||
* data will contain lineLength characters.
|
|
||||||
* If lineLength <=0, the encoded data is not divided into lines.
|
|
||||||
* If doEncode is false, lineLength is ignored.
|
|
||||||
* @param lineSeparator If doEncode is true, each line of encoded
|
|
||||||
* data will be terminated with this byte sequence (e.g. \r\n).
|
|
||||||
* If lineLength <= 0, the lineSeparator is not used.
|
|
||||||
* If doEncode is false lineSeparator is ignored.
|
|
||||||
*/
|
|
||||||
public Base64OutputStream(OutputStream out, boolean doEncode, int lineLength, byte[] lineSeparator) {
|
|
||||||
super(out);
|
|
||||||
this.doEncode = doEncode;
|
|
||||||
this.base64 = new Base64(lineLength, lineSeparator);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes the specified <code>byte</code> to this output stream.
|
|
||||||
*/
|
|
||||||
public void write(int i) throws IOException {
|
|
||||||
singleByte[0] = (byte) i;
|
|
||||||
write(singleByte, 0, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Writes <code>len</code> bytes from the specified
|
|
||||||
* <code>b</code> array starting at <code>offset</code> to
|
|
||||||
* this output stream.
|
|
||||||
*
|
|
||||||
* @param b source byte array
|
|
||||||
* @param offset where to start reading the bytes
|
|
||||||
* @param len maximum number of bytes to write
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs.
|
|
||||||
* @throws NullPointerException if the byte array parameter is null
|
|
||||||
* @throws IndexOutOfBoundsException if offset, len or buffer size are invalid
|
|
||||||
*/
|
|
||||||
public void write(byte b[], int offset, int len) throws IOException {
|
|
||||||
if (b == null) {
|
|
||||||
throw new NullPointerException();
|
|
||||||
} else if (offset < 0 || len < 0 || offset + len < 0) {
|
|
||||||
throw new IndexOutOfBoundsException();
|
|
||||||
} else if (offset > b.length || offset + len > b.length) {
|
|
||||||
throw new IndexOutOfBoundsException();
|
|
||||||
} else if (len > 0) {
|
|
||||||
if (doEncode) {
|
|
||||||
base64.encode(b, offset, len);
|
|
||||||
} else {
|
|
||||||
base64.decode(b, offset, len);
|
|
||||||
}
|
|
||||||
flush(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flushes this output stream and forces any buffered output bytes
|
|
||||||
* to be written out to the stream. If propogate is true, the wrapped
|
|
||||||
* stream will also be flushed.
|
|
||||||
*
|
|
||||||
* @param propogate boolean flag to indicate whether the wrapped
|
|
||||||
* OutputStream should also be flushed.
|
|
||||||
* @throws IOException if an I/O error occurs.
|
|
||||||
*/
|
|
||||||
private void flush(boolean propogate) throws IOException {
|
|
||||||
int avail = base64.avail();
|
|
||||||
if (avail > 0) {
|
|
||||||
byte[] buf = new byte[avail];
|
|
||||||
int c = base64.readResults(buf, 0, avail);
|
|
||||||
if (c > 0) {
|
|
||||||
out.write(buf, 0, c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (propogate) {
|
|
||||||
out.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flushes this output stream and forces any buffered output bytes
|
|
||||||
* to be written out to the stream.
|
|
||||||
*
|
|
||||||
* @throws IOException if an I/O error occurs.
|
|
||||||
*/
|
|
||||||
public void flush() throws IOException {
|
|
||||||
flush(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closes this output stream, flushing any remaining bytes that must be encoded. The
|
|
||||||
* underlying stream is flushed but not closed.
|
|
||||||
*/
|
|
||||||
public void close() throws IOException {
|
|
||||||
// Notify encoder of EOF (-1).
|
|
||||||
if (doEncode) {
|
|
||||||
base64.encode(singleByte, 0, -1);
|
|
||||||
} else {
|
|
||||||
base64.decode(singleByte, 0, -1);
|
|
||||||
}
|
|
||||||
flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,177 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.fsck.k9.activity.MessageCompose;
|
|
||||||
import com.fsck.k9.mail.internet.BinaryTempFileBody;
|
|
||||||
import com.fsck.k9.mail.internet.MimeMessage;
|
|
||||||
import com.fsck.k9.service.BootReceiver;
|
|
||||||
import com.fsck.k9.service.MailService;
|
|
||||||
|
|
||||||
public class k9 extends Application {
|
|
||||||
public static Application app = null;
|
|
||||||
public static File tempDirectory;
|
|
||||||
public static final String LOG_TAG = "k9";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this is enabled there will be additional logging information sent to
|
|
||||||
* Log.d, including protocol dumps.
|
|
||||||
*/
|
|
||||||
public static boolean DEBUG = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If this is enabled than logging that normally hides sensitive information
|
|
||||||
* like passwords will show that information.
|
|
||||||
*/
|
|
||||||
public static boolean DEBUG_SENSITIVE = false;
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The MIME type(s) of attachments we're willing to send. At the moment it is not possible
|
|
||||||
* to open a chooser with a list of filter types, so the chooser is only opened with the first
|
|
||||||
* item in the list. The entire list will be used to filter down attachments that are added
|
|
||||||
* with Intent.ACTION_SEND.
|
|
||||||
*/
|
|
||||||
public static final String[] ACCEPTABLE_ATTACHMENT_SEND_TYPES = new String[] {
|
|
||||||
"*/*",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The MIME type(s) of attachments we're willing to view.
|
|
||||||
*/
|
|
||||||
public static final String[] ACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
|
|
||||||
"image/*",
|
|
||||||
"audio/*",
|
|
||||||
"text/*",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The MIME type(s) of attachments we're not willing to view.
|
|
||||||
*/
|
|
||||||
public static final String[] UNACCEPTABLE_ATTACHMENT_VIEW_TYPES = new String[] {
|
|
||||||
"image/gif",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The MIME type(s) of attachments we're willing to download to SD.
|
|
||||||
*/
|
|
||||||
public static final String[] ACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
|
|
||||||
"*/*",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The MIME type(s) of attachments we're not willing to download to SD.
|
|
||||||
*/
|
|
||||||
public static final String[] UNACCEPTABLE_ATTACHMENT_DOWNLOAD_TYPES = new String[] {
|
|
||||||
"image/gif",
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The special name "INBOX" is used throughout the application to mean "Whatever folder
|
|
||||||
* the server refers to as the user's Inbox. Placed here to ease use.
|
|
||||||
*/
|
|
||||||
public static final String INBOX = "INBOX";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Specifies how many messages will be shown in a folder by default. This number is set
|
|
||||||
* on each new folder and can be incremented with "Load more messages..." by the
|
|
||||||
* VISIBLE_LIMIT_INCREMENT
|
|
||||||
*/
|
|
||||||
public static int DEFAULT_VISIBLE_LIMIT = 25;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Number of additioanl messages to load when a user selectes "Load more messages..."
|
|
||||||
*/
|
|
||||||
public static int VISIBLE_LIMIT_INCREMENT = 25;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The maximum size of an attachment we're willing to download (either View or Save)
|
|
||||||
* Attachments that are base64 encoded (most) will be about 1.375x their actual size
|
|
||||||
* so we should probably factor that in. A 5MB attachment will generally be around
|
|
||||||
* 6.8MB downloaded but only 5MB saved.
|
|
||||||
*/
|
|
||||||
public static final int MAX_ATTACHMENT_DOWNLOAD_SIZE = (5 * 1024 * 1024);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called throughout the application when the number of accounts has changed. This method
|
|
||||||
* enables or disables the Compose activity, the boot receiver and the service based on
|
|
||||||
* whether any accounts are configured.
|
|
||||||
*/
|
|
||||||
public static void setServicesEnabled(Context context) {
|
|
||||||
setServicesEnabled(context, Preferences.getPreferences(context).getAccounts().length > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setServicesEnabled(Context context, boolean enabled) {
|
|
||||||
PackageManager pm = context.getPackageManager();
|
|
||||||
if (!enabled && pm.getComponentEnabledSetting(new ComponentName(context, MailService.class)) ==
|
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
|
|
||||||
/*
|
|
||||||
* If no accounts now exist but the service is still enabled we're about to disable it
|
|
||||||
* so we'll reschedule to kill off any existing alarms.
|
|
||||||
*/
|
|
||||||
MailService.actionReschedule(context);
|
|
||||||
}
|
|
||||||
pm.setComponentEnabledSetting(
|
|
||||||
new ComponentName(context, MessageCompose.class),
|
|
||||||
enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
|
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
|
||||||
PackageManager.DONT_KILL_APP);
|
|
||||||
pm.setComponentEnabledSetting(
|
|
||||||
new ComponentName(context, BootReceiver.class),
|
|
||||||
enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
|
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
|
||||||
PackageManager.DONT_KILL_APP);
|
|
||||||
pm.setComponentEnabledSetting(
|
|
||||||
new ComponentName(context, MailService.class),
|
|
||||||
enabled ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
|
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
|
||||||
PackageManager.DONT_KILL_APP);
|
|
||||||
if (enabled && pm.getComponentEnabledSetting(new ComponentName(context, MailService.class)) ==
|
|
||||||
PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
|
|
||||||
/*
|
|
||||||
* And now if accounts do exist then we've just enabled the service and we want to
|
|
||||||
* schedule alarms for the new accounts.
|
|
||||||
*/
|
|
||||||
MailService.actionReschedule(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate() {
|
|
||||||
super.onCreate();
|
|
||||||
app = this;
|
|
||||||
Preferences prefs = Preferences.getPreferences(this);
|
|
||||||
DEBUG = prefs.geteEnableDebugLogging();
|
|
||||||
DEBUG_SENSITIVE = prefs.getEnableSensitiveLogging();
|
|
||||||
MessagingController.getInstance(this).resetVisibleLimits(prefs.getAccounts());
|
|
||||||
|
|
||||||
/*
|
|
||||||
* We have to give MimeMessage a temp directory because File.createTempFile(String, String)
|
|
||||||
* doesn't work in Android and MimeMessage does not have access to a Context.
|
|
||||||
*/
|
|
||||||
BinaryTempFileBody.setTempDirectory(getCacheDir());
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Enable background sync of messages
|
|
||||||
*/
|
|
||||||
|
|
||||||
setServicesEnabled(this);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,215 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.regex.Matcher;
|
|
||||||
import java.util.regex.Pattern;
|
|
||||||
|
|
||||||
import org.apache.james.mime4j.field.address.AddressList;
|
|
||||||
import org.apache.james.mime4j.field.address.Mailbox;
|
|
||||||
import org.apache.james.mime4j.field.address.MailboxList;
|
|
||||||
import org.apache.james.mime4j.field.address.NamedMailbox;
|
|
||||||
import org.apache.james.mime4j.field.address.parser.ParseException;
|
|
||||||
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.Utility;
|
|
||||||
import com.fsck.k9.mail.internet.MimeUtility;
|
|
||||||
|
|
||||||
public class Address {
|
|
||||||
String mAddress;
|
|
||||||
|
|
||||||
String mPersonal;
|
|
||||||
|
|
||||||
public Address(String address, String personal) {
|
|
||||||
this.mAddress = address;
|
|
||||||
this.mPersonal = personal;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Address(String address) {
|
|
||||||
this.mAddress = address;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAddress() {
|
|
||||||
return mAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setAddress(String address) {
|
|
||||||
this.mAddress = address;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPersonal() {
|
|
||||||
return mPersonal;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPersonal(String personal) {
|
|
||||||
this.mPersonal = personal;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a comma separated list of addresses in RFC-822 format and return an
|
|
||||||
* array of Address objects.
|
|
||||||
*
|
|
||||||
* @param addressList
|
|
||||||
* @return An array of 0 or more Addresses.
|
|
||||||
*/
|
|
||||||
public static Address[] parse(String addressList) {
|
|
||||||
ArrayList<Address> addresses = new ArrayList<Address>();
|
|
||||||
if (addressList == null) {
|
|
||||||
return new Address[] {};
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
MailboxList parsedList = AddressList.parse(addressList).flatten();
|
|
||||||
for (int i = 0, count = parsedList.size(); i < count; i++) {
|
|
||||||
org.apache.james.mime4j.field.address.Address address = parsedList.get(i);
|
|
||||||
if (address instanceof NamedMailbox) {
|
|
||||||
NamedMailbox namedMailbox = (NamedMailbox)address;
|
|
||||||
addresses.add(new Address(namedMailbox.getLocalPart() + "@"
|
|
||||||
+ namedMailbox.getDomain(), namedMailbox.getName()));
|
|
||||||
} else if (address instanceof Mailbox) {
|
|
||||||
Mailbox mailbox = (Mailbox)address;
|
|
||||||
addresses.add(new Address(mailbox.getLocalPart() + "@" + mailbox.getDomain()));
|
|
||||||
} else {
|
|
||||||
Log.e(k9.LOG_TAG, "Unknown address type from Mime4J: "
|
|
||||||
+ address.getClass().toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (ParseException pe) {
|
|
||||||
}
|
|
||||||
return addresses.toArray(new Address[] {});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (o instanceof Address) {
|
|
||||||
return getAddress().equals(((Address) o).getAddress());
|
|
||||||
}
|
|
||||||
return super.equals(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
if (mPersonal != null) {
|
|
||||||
if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) {
|
|
||||||
return Utility.quoteString(mPersonal) + " <" + mAddress + ">";
|
|
||||||
} else {
|
|
||||||
return mPersonal + " <" + mAddress + ">";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return mAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toString(Address[] addresses) {
|
|
||||||
if (addresses == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
for (int i = 0; i < addresses.length; i++) {
|
|
||||||
sb.append(addresses[i].toString());
|
|
||||||
if (i < addresses.length - 1) {
|
|
||||||
sb.append(',');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns either the personal portion of the Address or the address portion if the personal
|
|
||||||
* is not available.
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public String toFriendly() {
|
|
||||||
if (mPersonal != null && mPersonal.length() > 0) {
|
|
||||||
return mPersonal;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return mAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String toFriendly(Address[] addresses) {
|
|
||||||
if (addresses == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
for (int i = 0; i < addresses.length; i++) {
|
|
||||||
sb.append(addresses[i].toFriendly());
|
|
||||||
if (i < addresses.length - 1) {
|
|
||||||
sb.append(',');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Unpacks an address list previously packed with packAddressList()
|
|
||||||
* @param list
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static Address[] unpack(String addressList) {
|
|
||||||
if (addressList == null) {
|
|
||||||
return new Address[] { };
|
|
||||||
}
|
|
||||||
ArrayList<Address> addresses = new ArrayList<Address>();
|
|
||||||
int length = addressList.length();
|
|
||||||
int pairStartIndex = 0;
|
|
||||||
int pairEndIndex = 0;
|
|
||||||
int addressEndIndex = 0;
|
|
||||||
while (pairStartIndex < length) {
|
|
||||||
pairEndIndex = addressList.indexOf(',', pairStartIndex);
|
|
||||||
if (pairEndIndex == -1) {
|
|
||||||
pairEndIndex = length;
|
|
||||||
}
|
|
||||||
addressEndIndex = addressList.indexOf(';', pairStartIndex);
|
|
||||||
String address = null;
|
|
||||||
String personal = null;
|
|
||||||
if (addressEndIndex == -1 || addressEndIndex > pairEndIndex) {
|
|
||||||
address = Utility.fastUrlDecode(addressList.substring(pairStartIndex, pairEndIndex));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
address = Utility.fastUrlDecode(addressList.substring(pairStartIndex, addressEndIndex));
|
|
||||||
personal = Utility.fastUrlDecode(addressList.substring(addressEndIndex + 1, pairEndIndex));
|
|
||||||
}
|
|
||||||
addresses.add(new Address(address, personal));
|
|
||||||
pairStartIndex = pairEndIndex + 1;
|
|
||||||
}
|
|
||||||
return addresses.toArray(new Address[] { });
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Packs an address list into a String that is very quick to read
|
|
||||||
* and parse. Packed lists can be unpacked with unpackAddressList()
|
|
||||||
* The packed list is a comma seperated list of:
|
|
||||||
* URLENCODE(address)[;URLENCODE(personal)]
|
|
||||||
* @param list
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String pack(Address[] addresses) {
|
|
||||||
if (addresses == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
for (int i = 0, count = addresses.length; i < count; i++) {
|
|
||||||
Address address = addresses[i];
|
|
||||||
try {
|
|
||||||
sb.append(URLEncoder.encode(address.getAddress(), "UTF-8"));
|
|
||||||
if (address.getPersonal() != null) {
|
|
||||||
sb.append(';');
|
|
||||||
sb.append(URLEncoder.encode(address.getPersonal(), "UTF-8"));
|
|
||||||
}
|
|
||||||
if (i < count - 1) {
|
|
||||||
sb.append(',');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (UnsupportedEncodingException uee) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
public class AuthenticationFailedException extends MessagingException {
|
|
||||||
public static final long serialVersionUID = -1;
|
|
||||||
|
|
||||||
public AuthenticationFailedException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public AuthenticationFailedException(String message, Throwable throwable) {
|
|
||||||
super(message, throwable);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,11 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public interface Body {
|
|
||||||
public InputStream getInputStream() throws MessagingException;
|
|
||||||
public void writeTo(OutputStream out) throws IOException, MessagingException;
|
|
||||||
}
|
|
@ -1,10 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
public abstract class BodyPart implements Part {
|
|
||||||
protected Multipart mParent;
|
|
||||||
|
|
||||||
public Multipart getParent() {
|
|
||||||
return mParent;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
public class CertificateValidationException extends MessagingException {
|
|
||||||
public static final long serialVersionUID = -1;
|
|
||||||
|
|
||||||
public CertificateValidationException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CertificateValidationException(String message, Throwable throwable) {
|
|
||||||
super(message, throwable);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* <pre>
|
|
||||||
* A FetchProfile is a list of items that should be downloaded in bulk for a set of messages.
|
|
||||||
* FetchProfile can contain the following objects:
|
|
||||||
* FetchProfile.Item: Described below.
|
|
||||||
* Message: Indicates that the body of the entire message should be fetched.
|
|
||||||
* Synonymous with FetchProfile.Item.BODY.
|
|
||||||
* Part: Indicates that the given Part should be fetched. The provider
|
|
||||||
* is expected have previously created the given BodyPart and stored
|
|
||||||
* any information it needs to download the content.
|
|
||||||
* </pre>
|
|
||||||
*/
|
|
||||||
public class FetchProfile extends ArrayList {
|
|
||||||
/**
|
|
||||||
* Default items available for pre-fetching. It should be expected that any
|
|
||||||
* item fetched by using these items could potentially include all of the
|
|
||||||
* previous items.
|
|
||||||
*/
|
|
||||||
public enum Item {
|
|
||||||
/**
|
|
||||||
* Download the flags of the message.
|
|
||||||
*/
|
|
||||||
FLAGS,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the envelope of the message. This should include at minimum
|
|
||||||
* the size and the following headers: date, subject, from, content-type, to, cc
|
|
||||||
*/
|
|
||||||
ENVELOPE,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download the structure of the message. This maps directly to IMAP's BODYSTRUCTURE
|
|
||||||
* and may map to other providers.
|
|
||||||
* The provider should, if possible, fill in a properly formatted MIME structure in
|
|
||||||
* the message without actually downloading any message data. If the provider is not
|
|
||||||
* capable of this operation it should specifically set the body of the message to null
|
|
||||||
* so that upper levels can detect that a full body download is needed.
|
|
||||||
*/
|
|
||||||
STRUCTURE,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A sane portion of the entire message, cut off at a provider determined limit.
|
|
||||||
* This should generaly be around 50kB.
|
|
||||||
*/
|
|
||||||
BODY_SANE,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The entire message.
|
|
||||||
*/
|
|
||||||
BODY,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Flags that can be applied to Messages.
|
|
||||||
*/
|
|
||||||
public enum Flag {
|
|
||||||
DELETED,
|
|
||||||
SEEN,
|
|
||||||
ANSWERED,
|
|
||||||
FLAGGED,
|
|
||||||
DRAFT,
|
|
||||||
RECENT,
|
|
||||||
|
|
||||||
/*
|
|
||||||
* The following flags are for internal library use only.
|
|
||||||
* TODO Eventually we should creates a Flags class that extends ArrayList that allows
|
|
||||||
* these flags and Strings to represent user defined flags. At that point the below
|
|
||||||
* flags should become user defined flags.
|
|
||||||
*/
|
|
||||||
/**
|
|
||||||
* Delete and remove from the LocalStore immediately.
|
|
||||||
*/
|
|
||||||
X_DESTROYED,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sending of an unsent message failed. It will be retried. Used to show status.
|
|
||||||
*/
|
|
||||||
X_SEND_FAILED,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sending of an unsent message is in progress.
|
|
||||||
*/
|
|
||||||
X_SEND_IN_PROGRESS,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates that a message is fully downloaded from the server and can be viewed normally.
|
|
||||||
* This does not include attachments, which are never downloaded fully.
|
|
||||||
*/
|
|
||||||
X_DOWNLOADED_FULL,
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Indicates that a message is partially downloaded from the server and can be viewed but
|
|
||||||
* more content is available on the server.
|
|
||||||
* This does not include attachments, which are never downloaded fully.
|
|
||||||
*/
|
|
||||||
X_DOWNLOADED_PARTIAL,
|
|
||||||
}
|
|
@ -1,107 +0,0 @@
|
|||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
|
|
||||||
public abstract class Folder {
|
|
||||||
public enum OpenMode {
|
|
||||||
READ_WRITE, READ_ONLY,
|
|
||||||
}
|
|
||||||
|
|
||||||
public enum FolderType {
|
|
||||||
HOLDS_FOLDERS, HOLDS_MESSAGES,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Forces an open of the MailProvider. If the provider is already open this
|
|
||||||
* function returns without doing anything.
|
|
||||||
*
|
|
||||||
* @param mode READ_ONLY or READ_WRITE
|
|
||||||
*/
|
|
||||||
public abstract void open(OpenMode mode) throws MessagingException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Forces a close of the MailProvider. Any further access will attempt to
|
|
||||||
* reopen the MailProvider.
|
|
||||||
*
|
|
||||||
* @param expunge If true all deleted messages will be expunged.
|
|
||||||
*/
|
|
||||||
public abstract void close(boolean expunge) throws MessagingException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return True if further commands are not expected to have to open the
|
|
||||||
* connection.
|
|
||||||
*/
|
|
||||||
public abstract boolean isOpen();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the mode the folder was opened with. This may be different than the mode the open
|
|
||||||
* was requested with.
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public abstract OpenMode getMode() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract boolean create(FolderType type) throws MessagingException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new folder with a specified display limit. Not abstract to allow
|
|
||||||
* remote folders to not override or worry about this call if they don't care to.
|
|
||||||
*/
|
|
||||||
public boolean create(FolderType type, int displayLimit) throws MessagingException {
|
|
||||||
return create(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract boolean exists() throws MessagingException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return A count of the messages in the selected folder.
|
|
||||||
*/
|
|
||||||
public abstract int getMessageCount() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract int getUnreadMessageCount() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract Message getMessage(String uid) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract Message[] getMessages(int start, int end, MessageRetrievalListener listener)
|
|
||||||
throws MessagingException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the given list of messages. The specified listener is notified as
|
|
||||||
* each fetch completes. Messages are downloaded as (as) lightweight (as
|
|
||||||
* possible) objects to be filled in with later requests. In most cases this
|
|
||||||
* means that only the UID is downloaded.
|
|
||||||
*
|
|
||||||
* @param uids
|
|
||||||
* @param listener
|
|
||||||
*/
|
|
||||||
public abstract Message[] getMessages(MessageRetrievalListener listener)
|
|
||||||
throws MessagingException;
|
|
||||||
|
|
||||||
public abstract Message[] getMessages(String[] uids, MessageRetrievalListener listener)
|
|
||||||
throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void appendMessages(Message[] messages) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void copyMessages(Message[] msgs, Folder folder) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void setFlags(Message[] messages, Flag[] flags, boolean value)
|
|
||||||
throws MessagingException;
|
|
||||||
|
|
||||||
public abstract Message[] expunge() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void fetch(Message[] messages, FetchProfile fp,
|
|
||||||
MessageRetrievalListener listener) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void delete(boolean recurse) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract String getName();
|
|
||||||
|
|
||||||
public abstract Flag[] getPermanentFlags() throws MessagingException;
|
|
||||||
|
|
||||||
public boolean supportsFetchingFlags() {
|
|
||||||
return true;
|
|
||||||
}//isFlagSupported
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return getName();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,118 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
public abstract class Message implements Part, Body {
|
|
||||||
public enum RecipientType {
|
|
||||||
TO, CC, BCC,
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String mUid;
|
|
||||||
|
|
||||||
protected HashSet<Flag> mFlags = new HashSet<Flag>();
|
|
||||||
|
|
||||||
protected Date mInternalDate;
|
|
||||||
|
|
||||||
protected Folder mFolder;
|
|
||||||
|
|
||||||
public String getUid() {
|
|
||||||
return mUid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setUid(String uid) {
|
|
||||||
this.mUid = uid;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Folder getFolder() {
|
|
||||||
return mFolder;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract String getSubject() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void setSubject(String subject) throws MessagingException;
|
|
||||||
|
|
||||||
public Date getInternalDate() {
|
|
||||||
return mInternalDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setInternalDate(Date internalDate) {
|
|
||||||
this.mInternalDate = internalDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Date getReceivedDate() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract Date getSentDate() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void setSentDate(Date sentDate) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract Address[] getRecipients(RecipientType type) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void setRecipients(RecipientType type, Address[] addresses)
|
|
||||||
throws MessagingException;
|
|
||||||
|
|
||||||
public void setRecipient(RecipientType type, Address address) throws MessagingException {
|
|
||||||
setRecipients(type, new Address[] {
|
|
||||||
address
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Address[] getFrom() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void setFrom(Address from) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract Address[] getReplyTo() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void setReplyTo(Address[] from) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract Body getBody() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract String getContentType() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void addHeader(String name, String value) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void setHeader(String name, String value) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract String[] getHeader(String name) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void removeHeader(String name) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void setBody(Body body) throws MessagingException;
|
|
||||||
|
|
||||||
public boolean isMimeType(String mimeType) throws MessagingException {
|
|
||||||
return getContentType().startsWith(mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO Refactor Flags at some point to be able to store user defined flags.
|
|
||||||
*/
|
|
||||||
public Flag[] getFlags() {
|
|
||||||
return mFlags.toArray(new Flag[] {});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFlag(Flag flag, boolean set) throws MessagingException {
|
|
||||||
if (set) {
|
|
||||||
mFlags.add(flag);
|
|
||||||
} else {
|
|
||||||
mFlags.remove(flag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method calls setFlag(Flag, boolean)
|
|
||||||
* @param flags
|
|
||||||
* @param set
|
|
||||||
*/
|
|
||||||
public void setFlags(Flag[] flags, boolean set) throws MessagingException {
|
|
||||||
for (Flag flag : flags) {
|
|
||||||
setFlag(flag, set);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isSet(Flag flag) {
|
|
||||||
return mFlags.contains(flag);
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void saveChanges() throws MessagingException;
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
import java.util.Comparator;
|
|
||||||
|
|
||||||
public class MessageDateComparator implements Comparator<Message> {
|
|
||||||
public int compare(Message o1, Message o2) {
|
|
||||||
try {
|
|
||||||
if (o1.getSentDate() == null) {
|
|
||||||
return 1;
|
|
||||||
} else if (o2.getSentDate() == null) {
|
|
||||||
return -1;
|
|
||||||
} else
|
|
||||||
return o2.getSentDate().compareTo(o1.getSentDate());
|
|
||||||
} catch (Exception e) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
public interface MessageRetrievalListener {
|
|
||||||
public void messageStarted(String uid, int number, int ofTotal);
|
|
||||||
|
|
||||||
public void messageFinished(Message message, int number, int ofTotal);
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
public class MessagingException extends Exception {
|
|
||||||
public static final long serialVersionUID = -1;
|
|
||||||
|
|
||||||
public MessagingException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MessagingException(String message, Throwable throwable) {
|
|
||||||
super(message, throwable);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,48 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
public abstract class Multipart implements Body {
|
|
||||||
protected Part mParent;
|
|
||||||
|
|
||||||
protected ArrayList<BodyPart> mParts = new ArrayList<BodyPart>();
|
|
||||||
|
|
||||||
protected String mContentType;
|
|
||||||
|
|
||||||
public void addBodyPart(BodyPart part) throws MessagingException {
|
|
||||||
mParts.add(part);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addBodyPart(BodyPart part, int index) throws MessagingException {
|
|
||||||
mParts.add(index, part);
|
|
||||||
}
|
|
||||||
|
|
||||||
public BodyPart getBodyPart(int index) throws MessagingException {
|
|
||||||
return mParts.get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getContentType() throws MessagingException {
|
|
||||||
return mContentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCount() throws MessagingException {
|
|
||||||
return mParts.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean removeBodyPart(BodyPart part) throws MessagingException {
|
|
||||||
return mParts.remove(part);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeBodyPart(int index) throws MessagingException {
|
|
||||||
mParts.remove(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Part getParent() throws MessagingException {
|
|
||||||
return mParent;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setParent(Part parent) throws MessagingException {
|
|
||||||
this.mParent = parent;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,14 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
public class NoSuchProviderException extends MessagingException {
|
|
||||||
public static final long serialVersionUID = -1;
|
|
||||||
|
|
||||||
public NoSuchProviderException(String message) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public NoSuchProviderException(String message, Throwable throwable) {
|
|
||||||
super(message, throwable);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public interface Part {
|
|
||||||
public void addHeader(String name, String value) throws MessagingException;
|
|
||||||
|
|
||||||
public void removeHeader(String name) throws MessagingException;
|
|
||||||
|
|
||||||
public void setHeader(String name, String value) throws MessagingException;
|
|
||||||
|
|
||||||
public Body getBody() throws MessagingException;
|
|
||||||
|
|
||||||
public String getContentType() throws MessagingException;
|
|
||||||
|
|
||||||
public String getDisposition() throws MessagingException;
|
|
||||||
|
|
||||||
public String[] getHeader(String name) throws MessagingException;
|
|
||||||
|
|
||||||
public int getSize() throws MessagingException;
|
|
||||||
|
|
||||||
public boolean isMimeType(String mimeType) throws MessagingException;
|
|
||||||
|
|
||||||
public String getMimeType() throws MessagingException;
|
|
||||||
|
|
||||||
public void setBody(Body body) throws MessagingException;
|
|
||||||
|
|
||||||
public void writeTo(OutputStream out) throws IOException, MessagingException;
|
|
||||||
}
|
|
@ -1,80 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
import android.app.Application;
|
|
||||||
|
|
||||||
import com.fsck.k9.mail.store.ImapStore;
|
|
||||||
import com.fsck.k9.mail.store.LocalStore;
|
|
||||||
import com.fsck.k9.mail.store.Pop3Store;
|
|
||||||
import com.fsck.k9.mail.store.WebDavStore;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store is the access point for an email message store. It's location can be
|
|
||||||
* local or remote and no specific protocol is defined. Store is intended to
|
|
||||||
* loosely model in combination the JavaMail classes javax.mail.Store and
|
|
||||||
* javax.mail.Folder along with some additional functionality to improve
|
|
||||||
* performance on mobile devices. Implementations of this class should focus on
|
|
||||||
* making as few network connections as possible.
|
|
||||||
*/
|
|
||||||
public abstract class Store {
|
|
||||||
/**
|
|
||||||
* A global suggestion to Store implementors on how much of the body
|
|
||||||
* should be returned on FetchProfile.Item.BODY_SANE requests.
|
|
||||||
*/
|
|
||||||
public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (50 * 1024);
|
|
||||||
|
|
||||||
protected static final int SOCKET_CONNECT_TIMEOUT = 10000;
|
|
||||||
protected static final int SOCKET_READ_TIMEOUT = 60000;
|
|
||||||
|
|
||||||
private static HashMap<String, Store> mStores = new HashMap<String, Store>();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an instance of a mail store. The URI is parsed as a standard URI and
|
|
||||||
* the scheme is used to determine which protocol will be used. The
|
|
||||||
* following schemes are currently recognized: imap - IMAP with no
|
|
||||||
* connection security. Ex: imap://username:password@host/ imap+tls - IMAP
|
|
||||||
* with TLS connection security, if the server supports it. Ex:
|
|
||||||
* imap+tls://username:password@host imap+tls+ - IMAP with required TLS
|
|
||||||
* connection security. Connection fails if TLS is not available. Ex:
|
|
||||||
* imap+tls+://username:password@host imap+ssl+ - IMAP with required SSL
|
|
||||||
* connection security. Connection fails if SSL is not available. Ex:
|
|
||||||
* imap+ssl+://username:password@host
|
|
||||||
*
|
|
||||||
* @param uri The URI of the store.
|
|
||||||
* @return
|
|
||||||
* @throws MessagingException
|
|
||||||
*/
|
|
||||||
public synchronized static Store getInstance(String uri, Application application) throws MessagingException {
|
|
||||||
Store store = mStores.get(uri);
|
|
||||||
if (store == null) {
|
|
||||||
if (uri.startsWith("imap")) {
|
|
||||||
store = new ImapStore(uri);
|
|
||||||
} else if (uri.startsWith("pop3")) {
|
|
||||||
store = new Pop3Store(uri);
|
|
||||||
} else if (uri.startsWith("local")) {
|
|
||||||
store = new LocalStore(uri, application);
|
|
||||||
} else if (uri.startsWith("webdav")) {
|
|
||||||
store = new WebDavStore(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (store != null) {
|
|
||||||
mStores.put(uri, store);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (store == null) {
|
|
||||||
throw new MessagingException("Unable to locate an applicable Store for " + uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract Folder getFolder(String name) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract Folder[] getPersonalNamespaces() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void checkSettings() throws MessagingException;
|
|
||||||
}
|
|
@ -1,25 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail;
|
|
||||||
|
|
||||||
import com.fsck.k9.mail.transport.SmtpTransport;
|
|
||||||
import com.fsck.k9.mail.transport.WebDavTransport;
|
|
||||||
|
|
||||||
public abstract class Transport {
|
|
||||||
protected static final int SOCKET_CONNECT_TIMEOUT = 10000;
|
|
||||||
|
|
||||||
public synchronized static Transport getInstance(String uri) throws MessagingException {
|
|
||||||
if (uri.startsWith("smtp")) {
|
|
||||||
return new SmtpTransport(uri);
|
|
||||||
} else if (uri.startsWith("webdav")) {
|
|
||||||
return new WebDavTransport(uri);
|
|
||||||
} else {
|
|
||||||
throw new MessagingException("Unable to locate an applicable Transport for " + uri);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public abstract void open() throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void sendMessage(Message message) throws MessagingException;
|
|
||||||
|
|
||||||
public abstract void close() throws MessagingException;
|
|
||||||
}
|
|
@ -1,77 +0,0 @@
|
|||||||
package com.fsck.k9.mail.internet;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.FilterInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.codec.binary.Base64OutputStream;
|
|
||||||
import com.fsck.k9.mail.Body;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A Body that is backed by a temp file. The Body exposes a getOutputStream method that allows
|
|
||||||
* the user to write to the temp file. After the write the body is available via getInputStream
|
|
||||||
* and writeTo one time. After writeTo is called, or the InputStream returned from
|
|
||||||
* getInputStream is closed the file is deleted and the Body should be considered disposed of.
|
|
||||||
*/
|
|
||||||
public class BinaryTempFileBody implements Body {
|
|
||||||
private static File mTempDirectory;
|
|
||||||
|
|
||||||
private File mFile;
|
|
||||||
|
|
||||||
public static void setTempDirectory(File tempDirectory) {
|
|
||||||
mTempDirectory = tempDirectory;
|
|
||||||
}
|
|
||||||
|
|
||||||
public BinaryTempFileBody() throws IOException {
|
|
||||||
if (mTempDirectory == null) {
|
|
||||||
throw new
|
|
||||||
RuntimeException("setTempDirectory has not been called on BinaryTempFileBody!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public OutputStream getOutputStream() throws IOException {
|
|
||||||
mFile = File.createTempFile("body", null, mTempDirectory);
|
|
||||||
mFile.deleteOnExit();
|
|
||||||
return new FileOutputStream(mFile);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getInputStream() throws MessagingException {
|
|
||||||
try {
|
|
||||||
return new BinaryTempFileBodyInputStream(new FileInputStream(mFile));
|
|
||||||
}
|
|
||||||
catch (IOException ioe) {
|
|
||||||
throw new MessagingException("Unable to open body", ioe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeTo(OutputStream out) throws IOException, MessagingException {
|
|
||||||
InputStream in = getInputStream();
|
|
||||||
Base64OutputStream base64Out = new Base64OutputStream(out);
|
|
||||||
IOUtils.copy(in, base64Out);
|
|
||||||
base64Out.close();
|
|
||||||
mFile.delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
class BinaryTempFileBodyInputStream extends FilterInputStream {
|
|
||||||
public BinaryTempFileBodyInputStream(InputStream in) {
|
|
||||||
super(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close() throws IOException {
|
|
||||||
super.close();
|
|
||||||
mFile.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,121 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail.internet;
|
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
|
|
||||||
import com.fsck.k9.mail.Body;
|
|
||||||
import com.fsck.k9.mail.BodyPart;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO this is a close approximation of Message, need to update along with
|
|
||||||
* Message.
|
|
||||||
*/
|
|
||||||
public class MimeBodyPart extends BodyPart {
|
|
||||||
protected MimeHeader mHeader = new MimeHeader();
|
|
||||||
protected Body mBody;
|
|
||||||
protected int mSize;
|
|
||||||
|
|
||||||
public MimeBodyPart() throws MessagingException {
|
|
||||||
this(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MimeBodyPart(Body body) throws MessagingException {
|
|
||||||
this(body, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public MimeBodyPart(Body body, String mimeType) throws MessagingException {
|
|
||||||
if (mimeType != null) {
|
|
||||||
setHeader(MimeHeader.HEADER_CONTENT_TYPE, mimeType);
|
|
||||||
}
|
|
||||||
setBody(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getFirstHeader(String name) throws MessagingException {
|
|
||||||
return mHeader.getFirstHeader(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addHeader(String name, String value) throws MessagingException {
|
|
||||||
mHeader.addHeader(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHeader(String name, String value) throws MessagingException {
|
|
||||||
mHeader.setHeader(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String[] getHeader(String name) throws MessagingException {
|
|
||||||
return mHeader.getHeader(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeHeader(String name) throws MessagingException {
|
|
||||||
mHeader.removeHeader(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Body getBody() throws MessagingException {
|
|
||||||
return mBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBody(Body body) throws MessagingException {
|
|
||||||
this.mBody = body;
|
|
||||||
if (body instanceof com.fsck.k9.mail.Multipart) {
|
|
||||||
com.fsck.k9.mail.Multipart multipart = ((com.fsck.k9.mail.Multipart)body);
|
|
||||||
multipart.setParent(this);
|
|
||||||
setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
|
|
||||||
}
|
|
||||||
else if (body instanceof TextBody) {
|
|
||||||
String contentType = String.format("%s;\n charset=utf-8", getMimeType());
|
|
||||||
String name = MimeUtility.getHeaderParameter(getContentType(), "name");
|
|
||||||
if (name != null) {
|
|
||||||
contentType += String.format(";\n name=\"%s\"", name);
|
|
||||||
}
|
|
||||||
setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType);
|
|
||||||
setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getContentType() throws MessagingException {
|
|
||||||
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
|
|
||||||
if (contentType == null) {
|
|
||||||
return "text/plain";
|
|
||||||
} else {
|
|
||||||
return contentType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisposition() throws MessagingException {
|
|
||||||
String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
|
|
||||||
if (contentDisposition == null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return contentDisposition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMimeType() throws MessagingException {
|
|
||||||
return MimeUtility.getHeaderParameter(getContentType(), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMimeType(String mimeType) throws MessagingException {
|
|
||||||
return getMimeType().equals(mimeType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSize() throws MessagingException {
|
|
||||||
return mSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the MimeMessage out in MIME format.
|
|
||||||
*/
|
|
||||||
public void writeTo(OutputStream out) throws IOException, MessagingException {
|
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
|
|
||||||
mHeader.writeTo(out);
|
|
||||||
writer.write("\r\n");
|
|
||||||
writer.flush();
|
|
||||||
if (mBody != null) {
|
|
||||||
mBody.writeTo(out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,105 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail.internet;
|
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
|
|
||||||
import com.fsck.k9.Utility;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
|
|
||||||
public class MimeHeader {
|
|
||||||
/**
|
|
||||||
* Application specific header that contains Store specific information about an attachment.
|
|
||||||
* In IMAP this contains the IMAP BODYSTRUCTURE part id so that the ImapStore can later
|
|
||||||
* retrieve the attachment at will from the server.
|
|
||||||
* The info is recorded from this header on LocalStore.appendMessages and is put back
|
|
||||||
* into the MIME data by LocalStore.fetch.
|
|
||||||
*/
|
|
||||||
public static final String HEADER_ANDROID_ATTACHMENT_STORE_DATA = "X-Android-Attachment-StoreData";
|
|
||||||
|
|
||||||
public static final String HEADER_CONTENT_TYPE = "Content-Type";
|
|
||||||
public static final String HEADER_CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
|
|
||||||
public static final String HEADER_CONTENT_DISPOSITION = "Content-Disposition";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fields that should be omitted when writing the header using writeTo()
|
|
||||||
*/
|
|
||||||
private static final String[] writeOmitFields = {
|
|
||||||
// HEADER_ANDROID_ATTACHMENT_DOWNLOADED,
|
|
||||||
// HEADER_ANDROID_ATTACHMENT_ID,
|
|
||||||
HEADER_ANDROID_ATTACHMENT_STORE_DATA
|
|
||||||
};
|
|
||||||
|
|
||||||
protected ArrayList<Field> mFields = new ArrayList<Field>();
|
|
||||||
|
|
||||||
public void clear() {
|
|
||||||
mFields.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFirstHeader(String name) throws MessagingException {
|
|
||||||
String[] header = getHeader(name);
|
|
||||||
if (header == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return header[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addHeader(String name, String value) throws MessagingException {
|
|
||||||
mFields.add(new Field(name, MimeUtility.foldAndEncode(value)));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHeader(String name, String value) throws MessagingException {
|
|
||||||
if (name == null || value == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
removeHeader(name);
|
|
||||||
addHeader(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String[] getHeader(String name) throws MessagingException {
|
|
||||||
ArrayList<String> values = new ArrayList<String>();
|
|
||||||
for (Field field : mFields) {
|
|
||||||
if (field.name.equalsIgnoreCase(name)) {
|
|
||||||
values.add(field.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (values.size() == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return values.toArray(new String[] {});
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeHeader(String name) throws MessagingException {
|
|
||||||
ArrayList<Field> removeFields = new ArrayList<Field>();
|
|
||||||
for (Field field : mFields) {
|
|
||||||
if (field.name.equalsIgnoreCase(name)) {
|
|
||||||
removeFields.add(field);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mFields.removeAll(removeFields);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeTo(OutputStream out) throws IOException, MessagingException {
|
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
|
|
||||||
for (Field field : mFields) {
|
|
||||||
if (!Utility.arrayContains(writeOmitFields, field.name)) {
|
|
||||||
writer.write(field.name + ": " + field.value + "\r\n");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
writer.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
class Field {
|
|
||||||
String name;
|
|
||||||
|
|
||||||
String value;
|
|
||||||
|
|
||||||
public Field(String name, String value) {
|
|
||||||
this.name = name;
|
|
||||||
this.value = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,424 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail.internet;
|
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.Date;
|
|
||||||
import java.util.Stack;
|
|
||||||
|
|
||||||
import org.apache.james.mime4j.BodyDescriptor;
|
|
||||||
import org.apache.james.mime4j.ContentHandler;
|
|
||||||
import org.apache.james.mime4j.EOLConvertingInputStream;
|
|
||||||
import org.apache.james.mime4j.MimeStreamParser;
|
|
||||||
import org.apache.james.mime4j.field.DateTimeField;
|
|
||||||
import org.apache.james.mime4j.field.Field;
|
|
||||||
|
|
||||||
import com.fsck.k9.mail.Address;
|
|
||||||
import com.fsck.k9.mail.Body;
|
|
||||||
import com.fsck.k9.mail.BodyPart;
|
|
||||||
import com.fsck.k9.mail.Message;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
import com.fsck.k9.mail.Part;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An implementation of Message that stores all of it's metadata in RFC 822 and
|
|
||||||
* RFC 2045 style headers.
|
|
||||||
*/
|
|
||||||
public class MimeMessage extends Message {
|
|
||||||
protected MimeHeader mHeader = new MimeHeader();
|
|
||||||
protected Address[] mFrom;
|
|
||||||
protected Address[] mTo;
|
|
||||||
protected Address[] mCc;
|
|
||||||
protected Address[] mBcc;
|
|
||||||
protected Address[] mReplyTo;
|
|
||||||
protected Date mSentDate;
|
|
||||||
protected SimpleDateFormat mDateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z");
|
|
||||||
protected Body mBody;
|
|
||||||
protected int mSize;
|
|
||||||
|
|
||||||
public MimeMessage() {
|
|
||||||
/*
|
|
||||||
* Every new messages gets a Message-ID
|
|
||||||
*/
|
|
||||||
try {
|
|
||||||
setHeader("Message-ID", generateMessageId());
|
|
||||||
}
|
|
||||||
catch (MessagingException me) {
|
|
||||||
throw new RuntimeException("Unable to create MimeMessage", me);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String generateMessageId() {
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
sb.append("<");
|
|
||||||
for (int i = 0; i < 24; i++) {
|
|
||||||
sb.append(Integer.toString((int)(Math.random() * 35), 36));
|
|
||||||
}
|
|
||||||
sb.append(".");
|
|
||||||
sb.append(Long.toString(System.currentTimeMillis()));
|
|
||||||
sb.append("@email.android.com>");
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the given InputStream using Apache Mime4J to build a MimeMessage.
|
|
||||||
*
|
|
||||||
* @param in
|
|
||||||
* @throws IOException
|
|
||||||
* @throws MessagingException
|
|
||||||
*/
|
|
||||||
public MimeMessage(InputStream in) throws IOException, MessagingException {
|
|
||||||
parse(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void parse(InputStream in) throws IOException, MessagingException {
|
|
||||||
mHeader.clear();
|
|
||||||
mBody = null;
|
|
||||||
mBcc = null;
|
|
||||||
mTo = null;
|
|
||||||
mFrom = null;
|
|
||||||
mSentDate = null;
|
|
||||||
|
|
||||||
MimeStreamParser parser = new MimeStreamParser();
|
|
||||||
parser.setContentHandler(new MimeMessageBuilder());
|
|
||||||
parser.parse(new EOLConvertingInputStream(in));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getReceivedDate() throws MessagingException {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getSentDate() throws MessagingException {
|
|
||||||
if (mSentDate == null) {
|
|
||||||
try {
|
|
||||||
DateTimeField field = (DateTimeField)Field.parse("Date: "
|
|
||||||
+ MimeUtility.unfoldAndDecode(getFirstHeader("Date")));
|
|
||||||
mSentDate = field.getDate();
|
|
||||||
} catch (Exception e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mSentDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSentDate(Date sentDate) throws MessagingException {
|
|
||||||
setHeader("Date", mDateFormat.format(sentDate));
|
|
||||||
this.mSentDate = sentDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getContentType() throws MessagingException {
|
|
||||||
String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE);
|
|
||||||
if (contentType == null) {
|
|
||||||
return "text/plain";
|
|
||||||
} else {
|
|
||||||
return contentType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getDisposition() throws MessagingException {
|
|
||||||
String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION);
|
|
||||||
if (contentDisposition == null) {
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
return contentDisposition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getMimeType() throws MessagingException {
|
|
||||||
return MimeUtility.getHeaderParameter(getContentType(), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getSize() throws MessagingException {
|
|
||||||
return mSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of the given recipient type from this message. If no addresses are
|
|
||||||
* found the method returns an empty array.
|
|
||||||
*/
|
|
||||||
public Address[] getRecipients(RecipientType type) throws MessagingException {
|
|
||||||
if (type == RecipientType.TO) {
|
|
||||||
if (mTo == null) {
|
|
||||||
mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To")));
|
|
||||||
}
|
|
||||||
return mTo;
|
|
||||||
} else if (type == RecipientType.CC) {
|
|
||||||
if (mCc == null) {
|
|
||||||
mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC")));
|
|
||||||
}
|
|
||||||
return mCc;
|
|
||||||
} else if (type == RecipientType.BCC) {
|
|
||||||
if (mBcc == null) {
|
|
||||||
mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC")));
|
|
||||||
}
|
|
||||||
return mBcc;
|
|
||||||
} else {
|
|
||||||
throw new MessagingException("Unrecognized recipient type.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setRecipients(RecipientType type, Address[] addresses) throws MessagingException {
|
|
||||||
if (type == RecipientType.TO) {
|
|
||||||
if (addresses == null || addresses.length == 0) {
|
|
||||||
removeHeader("To");
|
|
||||||
this.mTo = null;
|
|
||||||
} else {
|
|
||||||
setHeader("To", Address.toString(addresses));
|
|
||||||
this.mTo = addresses;
|
|
||||||
}
|
|
||||||
} else if (type == RecipientType.CC) {
|
|
||||||
if (addresses == null || addresses.length == 0) {
|
|
||||||
removeHeader("CC");
|
|
||||||
this.mCc = null;
|
|
||||||
} else {
|
|
||||||
setHeader("CC", Address.toString(addresses));
|
|
||||||
this.mCc = addresses;
|
|
||||||
}
|
|
||||||
} else if (type == RecipientType.BCC) {
|
|
||||||
if (addresses == null || addresses.length == 0) {
|
|
||||||
removeHeader("BCC");
|
|
||||||
this.mBcc = null;
|
|
||||||
} else {
|
|
||||||
setHeader("BCC", Address.toString(addresses));
|
|
||||||
this.mBcc = addresses;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw new MessagingException("Unrecognized recipient type.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the unfolded, decoded value of the Subject header.
|
|
||||||
*/
|
|
||||||
public String getSubject() throws MessagingException {
|
|
||||||
return MimeUtility.unfoldAndDecode(getFirstHeader("Subject"));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSubject(String subject) throws MessagingException {
|
|
||||||
setHeader("Subject", subject);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Address[] getFrom() throws MessagingException {
|
|
||||||
if (mFrom == null) {
|
|
||||||
String list = MimeUtility.unfold(getFirstHeader("From"));
|
|
||||||
if (list == null || list.length() == 0) {
|
|
||||||
list = MimeUtility.unfold(getFirstHeader("Sender"));
|
|
||||||
}
|
|
||||||
mFrom = Address.parse(list);
|
|
||||||
}
|
|
||||||
return mFrom;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFrom(Address from) throws MessagingException {
|
|
||||||
if (from != null) {
|
|
||||||
setHeader("From", from.toString());
|
|
||||||
this.mFrom = new Address[] {
|
|
||||||
from
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.mFrom = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Address[] getReplyTo() throws MessagingException {
|
|
||||||
if (mReplyTo == null) {
|
|
||||||
mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to")));
|
|
||||||
}
|
|
||||||
return mReplyTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setReplyTo(Address[] replyTo) throws MessagingException {
|
|
||||||
if (replyTo == null || replyTo.length == 0) {
|
|
||||||
removeHeader("Reply-to");
|
|
||||||
mReplyTo = null;
|
|
||||||
} else {
|
|
||||||
setHeader("Reply-to", Address.toString(replyTo));
|
|
||||||
mReplyTo = replyTo;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void saveChanges() throws MessagingException {
|
|
||||||
throw new MessagingException("saveChanges not yet implemented");
|
|
||||||
}
|
|
||||||
|
|
||||||
public Body getBody() throws MessagingException {
|
|
||||||
return mBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setBody(Body body) throws MessagingException {
|
|
||||||
this.mBody = body;
|
|
||||||
if (body instanceof com.fsck.k9.mail.Multipart) {
|
|
||||||
com.fsck.k9.mail.Multipart multipart = ((com.fsck.k9.mail.Multipart)body);
|
|
||||||
multipart.setParent(this);
|
|
||||||
setHeader(MimeHeader.HEADER_CONTENT_TYPE, multipart.getContentType());
|
|
||||||
setHeader("MIME-Version", "1.0");
|
|
||||||
}
|
|
||||||
else if (body instanceof TextBody) {
|
|
||||||
setHeader(MimeHeader.HEADER_CONTENT_TYPE, String.format("%s;\n charset=utf-8",
|
|
||||||
getMimeType()));
|
|
||||||
setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, "base64");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected String getFirstHeader(String name) throws MessagingException {
|
|
||||||
return mHeader.getFirstHeader(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addHeader(String name, String value) throws MessagingException {
|
|
||||||
mHeader.addHeader(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setHeader(String name, String value) throws MessagingException {
|
|
||||||
mHeader.setHeader(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String[] getHeader(String name) throws MessagingException {
|
|
||||||
return mHeader.getHeader(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeHeader(String name) throws MessagingException {
|
|
||||||
mHeader.removeHeader(name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeTo(OutputStream out) throws IOException, MessagingException {
|
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
|
|
||||||
mHeader.writeTo(out);
|
|
||||||
writer.write("\r\n");
|
|
||||||
writer.flush();
|
|
||||||
if (mBody != null) {
|
|
||||||
mBody.writeTo(out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getInputStream() throws MessagingException {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class MimeMessageBuilder implements ContentHandler {
|
|
||||||
private Stack stack = new Stack();
|
|
||||||
|
|
||||||
public MimeMessageBuilder() {
|
|
||||||
}
|
|
||||||
|
|
||||||
private void expect(Class c) {
|
|
||||||
if (!c.isInstance(stack.peek())) {
|
|
||||||
throw new IllegalStateException("Internal stack error: " + "Expected '"
|
|
||||||
+ c.getName() + "' found '" + stack.peek().getClass().getName() + "'");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startMessage() {
|
|
||||||
if (stack.isEmpty()) {
|
|
||||||
stack.push(MimeMessage.this);
|
|
||||||
} else {
|
|
||||||
expect(Part.class);
|
|
||||||
try {
|
|
||||||
MimeMessage m = new MimeMessage();
|
|
||||||
((Part)stack.peek()).setBody(m);
|
|
||||||
stack.push(m);
|
|
||||||
} catch (MessagingException me) {
|
|
||||||
throw new Error(me);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void endMessage() {
|
|
||||||
expect(MimeMessage.class);
|
|
||||||
stack.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startHeader() {
|
|
||||||
expect(Part.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void field(String fieldData) {
|
|
||||||
expect(Part.class);
|
|
||||||
try {
|
|
||||||
String[] tokens = fieldData.split(":", 2);
|
|
||||||
((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim());
|
|
||||||
} catch (MessagingException me) {
|
|
||||||
throw new Error(me);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void endHeader() {
|
|
||||||
expect(Part.class);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startMultipart(BodyDescriptor bd) {
|
|
||||||
expect(Part.class);
|
|
||||||
|
|
||||||
Part e = (Part)stack.peek();
|
|
||||||
try {
|
|
||||||
MimeMultipart multiPart = new MimeMultipart(e.getContentType());
|
|
||||||
e.setBody(multiPart);
|
|
||||||
stack.push(multiPart);
|
|
||||||
} catch (MessagingException me) {
|
|
||||||
throw new Error(me);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void body(BodyDescriptor bd, InputStream in) throws IOException {
|
|
||||||
expect(Part.class);
|
|
||||||
Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding());
|
|
||||||
try {
|
|
||||||
((Part)stack.peek()).setBody(body);
|
|
||||||
} catch (MessagingException me) {
|
|
||||||
throw new Error(me);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void endMultipart() {
|
|
||||||
stack.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void startBodyPart() {
|
|
||||||
expect(MimeMultipart.class);
|
|
||||||
|
|
||||||
try {
|
|
||||||
MimeBodyPart bodyPart = new MimeBodyPart();
|
|
||||||
((MimeMultipart)stack.peek()).addBodyPart(bodyPart);
|
|
||||||
stack.push(bodyPart);
|
|
||||||
} catch (MessagingException me) {
|
|
||||||
throw new Error(me);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void endBodyPart() {
|
|
||||||
expect(BodyPart.class);
|
|
||||||
stack.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void epilogue(InputStream is) throws IOException {
|
|
||||||
expect(MimeMultipart.class);
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
int b;
|
|
||||||
while ((b = is.read()) != -1) {
|
|
||||||
sb.append((char)b);
|
|
||||||
}
|
|
||||||
// ((Multipart) stack.peek()).setEpilogue(sb.toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void preamble(InputStream is) throws IOException {
|
|
||||||
expect(MimeMultipart.class);
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
int b;
|
|
||||||
while ((b = is.read()) != -1) {
|
|
||||||
sb.append((char)b);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
((MimeMultipart)stack.peek()).setPreamble(sb.toString());
|
|
||||||
} catch (MessagingException me) {
|
|
||||||
throw new Error(me);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void raw(InputStream is) throws IOException {
|
|
||||||
throw new UnsupportedOperationException("Not supported");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,95 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail.internet;
|
|
||||||
|
|
||||||
import java.io.BufferedWriter;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.OutputStreamWriter;
|
|
||||||
|
|
||||||
import com.fsck.k9.mail.BodyPart;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
import com.fsck.k9.mail.Multipart;
|
|
||||||
|
|
||||||
public class MimeMultipart extends Multipart {
|
|
||||||
protected String mPreamble;
|
|
||||||
|
|
||||||
protected String mContentType;
|
|
||||||
|
|
||||||
protected String mBoundary;
|
|
||||||
|
|
||||||
protected String mSubType;
|
|
||||||
|
|
||||||
public MimeMultipart() throws MessagingException {
|
|
||||||
mBoundary = generateBoundary();
|
|
||||||
setSubType("mixed");
|
|
||||||
}
|
|
||||||
|
|
||||||
public MimeMultipart(String contentType) throws MessagingException {
|
|
||||||
this.mContentType = contentType;
|
|
||||||
try {
|
|
||||||
mSubType = MimeUtility.getHeaderParameter(contentType, null).split("/")[1];
|
|
||||||
mBoundary = MimeUtility.getHeaderParameter(contentType, "boundary");
|
|
||||||
if (mBoundary == null) {
|
|
||||||
throw new MessagingException("MultiPart does not contain boundary: " + contentType);
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
throw new MessagingException(
|
|
||||||
"Invalid MultiPart Content-Type; must contain subtype and boundary. ("
|
|
||||||
+ contentType + ")", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String generateBoundary() {
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
sb.append("----");
|
|
||||||
for (int i = 0; i < 30; i++) {
|
|
||||||
sb.append(Integer.toString((int)(Math.random() * 35), 36));
|
|
||||||
}
|
|
||||||
return sb.toString().toUpperCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getPreamble() throws MessagingException {
|
|
||||||
return mPreamble;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setPreamble(String preamble) throws MessagingException {
|
|
||||||
this.mPreamble = preamble;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getContentType() throws MessagingException {
|
|
||||||
return mContentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSubType(String subType) throws MessagingException {
|
|
||||||
this.mSubType = subType;
|
|
||||||
mContentType = String.format("multipart/%s; boundary=\"%s\"", subType, mBoundary);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeTo(OutputStream out) throws IOException, MessagingException {
|
|
||||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024);
|
|
||||||
|
|
||||||
if (mPreamble != null) {
|
|
||||||
writer.write(mPreamble + "\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(mParts.size() == 0){
|
|
||||||
writer.write("--" + mBoundary + "\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
for (int i = 0, count = mParts.size(); i < count; i++) {
|
|
||||||
BodyPart bodyPart = (BodyPart)mParts.get(i);
|
|
||||||
writer.write("--" + mBoundary + "\r\n");
|
|
||||||
writer.flush();
|
|
||||||
bodyPart.writeTo(out);
|
|
||||||
writer.write("\r\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.write("--" + mBoundary + "--\r\n");
|
|
||||||
writer.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getInputStream() throws MessagingException {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,311 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail.internet;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.nio.charset.Charset;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import org.apache.commons.io.IOUtils;
|
|
||||||
import org.apache.james.mime4j.decoder.Base64InputStream;
|
|
||||||
import org.apache.james.mime4j.decoder.DecoderUtil;
|
|
||||||
import org.apache.james.mime4j.decoder.QuotedPrintableInputStream;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.mail.Body;
|
|
||||||
import com.fsck.k9.mail.BodyPart;
|
|
||||||
import com.fsck.k9.mail.Message;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
import com.fsck.k9.mail.Multipart;
|
|
||||||
import com.fsck.k9.mail.Part;
|
|
||||||
|
|
||||||
public class MimeUtility {
|
|
||||||
|
|
||||||
|
|
||||||
public static String unfold(String s) {
|
|
||||||
if (s == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return s.replaceAll("\r|\n", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String decode(String s) {
|
|
||||||
if (s == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return DecoderUtil.decodeEncodedWords(s);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static String unfoldAndDecode(String s) {
|
|
||||||
return decode(unfold(s));
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO implement proper foldAndEncode
|
|
||||||
public static String foldAndEncode(String s) {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the named parameter of a header field. If name is null the first
|
|
||||||
* parameter is returned, or if there are no additional parameters in the
|
|
||||||
* field the entire field is returned. Otherwise the named parameter is
|
|
||||||
* searched for in a case insensitive fashion and returned. If the parameter
|
|
||||||
* cannot be found the method returns null.
|
|
||||||
*
|
|
||||||
* @param header
|
|
||||||
* @param name
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static String getHeaderParameter(String header, String name) {
|
|
||||||
if (header == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
header = header.replaceAll("\r|\n", "");
|
|
||||||
String[] parts = header.split(";");
|
|
||||||
if (name == null) {
|
|
||||||
return parts[0];
|
|
||||||
}
|
|
||||||
for (String part : parts) {
|
|
||||||
if (part.trim().toLowerCase().startsWith(name.toLowerCase())) {
|
|
||||||
String parameter = part.split("=", 2)[1].trim();
|
|
||||||
if (parameter.startsWith("\"") && parameter.endsWith("\"")) {
|
|
||||||
return parameter.substring(1, parameter.length() - 1);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return parameter;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Part findFirstPartByMimeType(Part part, String mimeType)
|
|
||||||
throws MessagingException {
|
|
||||||
if (part.getBody() instanceof Multipart) {
|
|
||||||
Multipart multipart = (Multipart)part.getBody();
|
|
||||||
for (int i = 0, count = multipart.getCount(); i < count; i++) {
|
|
||||||
BodyPart bodyPart = multipart.getBodyPart(i);
|
|
||||||
Part ret = findFirstPartByMimeType(bodyPart, mimeType);
|
|
||||||
if (ret != null) {
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (part.getMimeType().equalsIgnoreCase(mimeType)) {
|
|
||||||
return part;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Part findPartByContentId(Part part, String contentId) throws Exception {
|
|
||||||
if (part.getBody() instanceof Multipart) {
|
|
||||||
Multipart multipart = (Multipart)part.getBody();
|
|
||||||
for (int i = 0, count = multipart.getCount(); i < count; i++) {
|
|
||||||
BodyPart bodyPart = multipart.getBodyPart(i);
|
|
||||||
Part ret = findPartByContentId(bodyPart, contentId);
|
|
||||||
if (ret != null) {
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String[] header = part.getHeader("Content-ID");
|
|
||||||
if (header != null) {
|
|
||||||
for (String s : header) {
|
|
||||||
if (s.equals(contentId)) {
|
|
||||||
return part;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the Part's body and returns a String based on any charset conversion that needed
|
|
||||||
* to be done.
|
|
||||||
* @param part
|
|
||||||
* @return
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public static String getTextFromPart(Part part) {
|
|
||||||
Charset mCharsetConverter;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (part != null && part.getBody() != null) {
|
|
||||||
InputStream in = part.getBody().getInputStream();
|
|
||||||
String mimeType = part.getMimeType();
|
|
||||||
if (mimeType != null && MimeUtility.mimeTypeMatches(mimeType, "text/*")) {
|
|
||||||
/*
|
|
||||||
* Now we read the part into a buffer for further processing. Because
|
|
||||||
* the stream is now wrapped we'll remove any transfer encoding at this point.
|
|
||||||
*/
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
|
||||||
IOUtils.copy(in, out);
|
|
||||||
|
|
||||||
byte[] bytes = out.toByteArray();
|
|
||||||
in.close();
|
|
||||||
out.close();
|
|
||||||
|
|
||||||
String charset = getHeaderParameter(part.getContentType(), "charset");
|
|
||||||
/*
|
|
||||||
* We've got a text part, so let's see if it needs to be processed further.
|
|
||||||
*/
|
|
||||||
if (charset != null) {
|
|
||||||
/*
|
|
||||||
* See if there is conversion from the MIME charset to the Java one.
|
|
||||||
*/
|
|
||||||
mCharsetConverter = Charset.forName(charset);
|
|
||||||
charset = mCharsetConverter.name();
|
|
||||||
}
|
|
||||||
if (charset != null) {
|
|
||||||
/*
|
|
||||||
* We've got a charset encoding, so decode using it.
|
|
||||||
*/
|
|
||||||
return new String(bytes, 0, bytes.length, charset);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
/*
|
|
||||||
* No encoding, so use us-ascii, which is the standard.
|
|
||||||
*/
|
|
||||||
return new String(bytes, 0, bytes.length, "ASCII");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
/*
|
|
||||||
* If we are not able to process the body there's nothing we can do about it. Return
|
|
||||||
* null and let the upper layers handle the missing content.
|
|
||||||
*/
|
|
||||||
Log.e(k9.LOG_TAG, "Unable to getTextFromPart", e);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given mimeType matches the matchAgainst specification.
|
|
||||||
* @param mimeType A MIME type to check.
|
|
||||||
* @param matchAgainst A MIME type to check against. May include wildcards such as image/* or
|
|
||||||
* * /*.
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static boolean mimeTypeMatches(String mimeType, String matchAgainst) {
|
|
||||||
return mimeType.matches(matchAgainst.replaceAll("\\*", "\\.\\*"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true if the given mimeType matches any of the matchAgainst specifications.
|
|
||||||
* @param mimeType A MIME type to check.
|
|
||||||
* @param matchAgainst An array of MIME types to check against. May include wildcards such
|
|
||||||
* as image/* or * /*.
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) {
|
|
||||||
for (String matchType : matchAgainst) {
|
|
||||||
if (mimeType.matches(matchType.replaceAll("\\*", "\\.\\*"))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes any content transfer encoding from the stream and returns a Body.
|
|
||||||
*/
|
|
||||||
public static Body decodeBody(InputStream in, String contentTransferEncoding)
|
|
||||||
throws IOException {
|
|
||||||
/*
|
|
||||||
* We'll remove any transfer encoding by wrapping the stream.
|
|
||||||
*/
|
|
||||||
if (contentTransferEncoding != null) {
|
|
||||||
contentTransferEncoding =
|
|
||||||
MimeUtility.getHeaderParameter(contentTransferEncoding, null);
|
|
||||||
if ("quoted-printable".equalsIgnoreCase(contentTransferEncoding)) {
|
|
||||||
in = new QuotedPrintableInputStream(in);
|
|
||||||
}
|
|
||||||
else if ("base64".equalsIgnoreCase(contentTransferEncoding)) {
|
|
||||||
in = new Base64InputStream(in);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
BinaryTempFileBody tempBody = new BinaryTempFileBody();
|
|
||||||
OutputStream out = tempBody.getOutputStream();
|
|
||||||
IOUtils.copy(in, out);
|
|
||||||
out.close();
|
|
||||||
return tempBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An unfortunately named method that makes decisions about a Part (usually a Message)
|
|
||||||
* as to which of it's children will be "viewable" and which will be attachments.
|
|
||||||
* The method recursively sorts the viewables and attachments into seperate
|
|
||||||
* lists for further processing.
|
|
||||||
* @param part
|
|
||||||
* @param viewables
|
|
||||||
* @param attachments
|
|
||||||
* @throws MessagingException
|
|
||||||
*/
|
|
||||||
public static void collectParts(Part part, ArrayList<Part> viewables,
|
|
||||||
ArrayList<Part> attachments) throws MessagingException {
|
|
||||||
String disposition = part.getDisposition();
|
|
||||||
String dispositionType = null;
|
|
||||||
String dispositionFilename = null;
|
|
||||||
if (disposition != null) {
|
|
||||||
dispositionType = MimeUtility.getHeaderParameter(disposition, null);
|
|
||||||
dispositionFilename = MimeUtility.getHeaderParameter(disposition, "filename");
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* A best guess that this part is intended to be an attachment and not inline.
|
|
||||||
*/
|
|
||||||
boolean attachment = ("attachment".equalsIgnoreCase(dispositionType))
|
|
||||||
|| (dispositionFilename != null)
|
|
||||||
&& (!"inline".equalsIgnoreCase(dispositionType));
|
|
||||||
|
|
||||||
/*
|
|
||||||
* If the part is Multipart but not alternative it's either mixed or
|
|
||||||
* something we don't know about, which means we treat it as mixed
|
|
||||||
* per the spec. We just process it's pieces recursively.
|
|
||||||
*/
|
|
||||||
if (part.getBody() instanceof Multipart) {
|
|
||||||
Multipart mp = (Multipart)part.getBody();
|
|
||||||
for (int i = 0; i < mp.getCount(); i++) {
|
|
||||||
collectParts(mp.getBodyPart(i), viewables, attachments);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* If the part is an embedded message we just continue to process
|
|
||||||
* it, pulling any viewables or attachments into the running list.
|
|
||||||
*/
|
|
||||||
else if (part.getBody() instanceof Message) {
|
|
||||||
Message message = (Message)part.getBody();
|
|
||||||
collectParts(message, viewables, attachments);
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* If the part is HTML and it got this far it's part of a mixed (et
|
|
||||||
* al) and should be rendered inline.
|
|
||||||
*/
|
|
||||||
else if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/html"))) {
|
|
||||||
viewables.add(part);
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* If the part is plain text and it got this far it's part of a
|
|
||||||
* mixed (et al) and should be rendered inline.
|
|
||||||
*/
|
|
||||||
else if ((!attachment) && (part.getMimeType().equalsIgnoreCase("text/plain"))) {
|
|
||||||
viewables.add(part);
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* Finally, if it's nothing else we will include it as an attachment.
|
|
||||||
*/
|
|
||||||
else {
|
|
||||||
attachments.add(part);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail.internet;
|
|
||||||
|
|
||||||
import java.io.ByteArrayInputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
|
|
||||||
|
|
||||||
import com.fsck.k9.codec.binary.Base64;
|
|
||||||
import com.fsck.k9.mail.Body;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
|
|
||||||
public class TextBody implements Body {
|
|
||||||
String mBody;
|
|
||||||
|
|
||||||
public TextBody(String body) {
|
|
||||||
this.mBody = body;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeTo(OutputStream out) throws IOException, MessagingException {
|
|
||||||
byte[] bytes = mBody.getBytes("UTF-8");
|
|
||||||
out.write(Base64.encodeBase64Chunked(bytes));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the text of the body in it's unencoded format.
|
|
||||||
* @return
|
|
||||||
*/
|
|
||||||
public String getText() {
|
|
||||||
return mBody;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an InputStream that reads this body's text in UTF-8 format.
|
|
||||||
*/
|
|
||||||
public InputStream getInputStream() throws MessagingException {
|
|
||||||
try {
|
|
||||||
byte[] b = mBody.getBytes("UTF-8");
|
|
||||||
return new ByteArrayInputStream(b);
|
|
||||||
}
|
|
||||||
catch (UnsupportedEncodingException usee) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,356 +0,0 @@
|
|||||||
/**
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.fsck.k9.mail.store;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.text.ParseException;
|
|
||||||
import java.text.SimpleDateFormat;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Date;
|
|
||||||
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.FixedLengthInputStream;
|
|
||||||
import com.fsck.k9.PeekableInputStream;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
|
|
||||||
public class ImapResponseParser {
|
|
||||||
SimpleDateFormat mDateTimeFormat = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z");
|
|
||||||
PeekableInputStream mIn;
|
|
||||||
InputStream mActiveLiteral;
|
|
||||||
|
|
||||||
public ImapResponseParser(PeekableInputStream in) {
|
|
||||||
this.mIn = in;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the next response available on the stream and returns an
|
|
||||||
* ImapResponse object that represents it.
|
|
||||||
*
|
|
||||||
* @return
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public ImapResponse readResponse() throws IOException {
|
|
||||||
ImapResponse response = new ImapResponse();
|
|
||||||
if (mActiveLiteral != null) {
|
|
||||||
while (mActiveLiteral.read() != -1)
|
|
||||||
;
|
|
||||||
mActiveLiteral = null;
|
|
||||||
}
|
|
||||||
int ch = mIn.peek();
|
|
||||||
if (ch == '*') {
|
|
||||||
parseUntaggedResponse();
|
|
||||||
readTokens(response);
|
|
||||||
} else if (ch == '+') {
|
|
||||||
response.mCommandContinuationRequested =
|
|
||||||
parseCommandContinuationRequest();
|
|
||||||
readTokens(response);
|
|
||||||
} else {
|
|
||||||
response.mTag = parseTaggedResponse();
|
|
||||||
readTokens(response);
|
|
||||||
}
|
|
||||||
if (Config.LOGD) {
|
|
||||||
if (k9.DEBUG) {
|
|
||||||
Log.d(k9.LOG_TAG, "<<< " + response.toString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void readTokens(ImapResponse response) throws IOException {
|
|
||||||
response.clear();
|
|
||||||
Object token;
|
|
||||||
while ((token = readToken()) != null) {
|
|
||||||
if (response != null) {
|
|
||||||
response.add(token);
|
|
||||||
}
|
|
||||||
if (mActiveLiteral != null) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
response.mCompleted = token == null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the next token of the response. The token can be one of: String -
|
|
||||||
* for NIL, QUOTED, NUMBER, ATOM. InputStream - for LITERAL.
|
|
||||||
* InputStream.available() returns the total length of the stream.
|
|
||||||
* ImapResponseList - for PARENTHESIZED LIST. Can contain any of the above
|
|
||||||
* elements including List.
|
|
||||||
*
|
|
||||||
* @return The next token in the response or null if there are no more
|
|
||||||
* tokens.
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
public Object readToken() throws IOException {
|
|
||||||
while (true) {
|
|
||||||
Object token = parseToken();
|
|
||||||
if (token == null || !token.equals(")")) {
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Object parseToken() throws IOException {
|
|
||||||
if (mActiveLiteral != null) {
|
|
||||||
while (mActiveLiteral.read() != -1)
|
|
||||||
;
|
|
||||||
mActiveLiteral = null;
|
|
||||||
}
|
|
||||||
while (true) {
|
|
||||||
int ch = mIn.peek();
|
|
||||||
if (ch == '(') {
|
|
||||||
return parseList();
|
|
||||||
} else if (ch == ')') {
|
|
||||||
expect(')');
|
|
||||||
return ")";
|
|
||||||
} else if (ch == '"') {
|
|
||||||
return parseQuoted();
|
|
||||||
} else if (ch == '{') {
|
|
||||||
mActiveLiteral = parseLiteral();
|
|
||||||
return mActiveLiteral;
|
|
||||||
} else if (ch == ' ') {
|
|
||||||
expect(' ');
|
|
||||||
} else if (ch == '\r') {
|
|
||||||
expect('\r');
|
|
||||||
expect('\n');
|
|
||||||
return null;
|
|
||||||
} else if (ch == '\n') {
|
|
||||||
expect('\n');
|
|
||||||
return null;
|
|
||||||
} else if (ch == '\t') {
|
|
||||||
expect('\t');
|
|
||||||
} else {
|
|
||||||
return parseAtom();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean parseCommandContinuationRequest() throws IOException {
|
|
||||||
expect('+');
|
|
||||||
expect(' ');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// * OK [UIDNEXT 175] Predicted next UID
|
|
||||||
private void parseUntaggedResponse() throws IOException {
|
|
||||||
expect('*');
|
|
||||||
expect(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3 OK [READ-WRITE] Select completed.
|
|
||||||
private String parseTaggedResponse() throws IOException {
|
|
||||||
String tag = readStringUntil(' ');
|
|
||||||
return tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
private ImapList parseList() throws IOException {
|
|
||||||
expect('(');
|
|
||||||
ImapList list = new ImapList();
|
|
||||||
Object token;
|
|
||||||
while (true) {
|
|
||||||
token = parseToken();
|
|
||||||
if (token == null) {
|
|
||||||
break;
|
|
||||||
} else if (token instanceof InputStream) {
|
|
||||||
list.add(token);
|
|
||||||
break;
|
|
||||||
} else if (token.equals(")")) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
list.add(token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return list;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String parseAtom() throws IOException {
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
int ch;
|
|
||||||
while (true) {
|
|
||||||
ch = mIn.peek();
|
|
||||||
if (ch == -1) {
|
|
||||||
throw new IOException("parseAtom(): end of stream reached");
|
|
||||||
} else if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' ||
|
|
||||||
// docs claim that flags are \ atom but atom isn't supposed to
|
|
||||||
// contain
|
|
||||||
// * and some falgs contain *
|
|
||||||
// ch == '%' || ch == '*' ||
|
|
||||||
ch == '%' ||
|
|
||||||
// TODO probably should not allow \ and should recognize
|
|
||||||
// it as a flag instead
|
|
||||||
// ch == '"' || ch == '\' ||
|
|
||||||
ch == '"' || (ch >= 0x00 && ch <= 0x1f) || ch == 0x7f) {
|
|
||||||
if (sb.length() == 0) {
|
|
||||||
throw new IOException(String.format("parseAtom(): (%04x %c)", (int)ch, ch));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
} else {
|
|
||||||
sb.append((char)mIn.read());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A { has been read, read the rest of the size string, the space and then
|
|
||||||
* notify the listener with an InputStream.
|
|
||||||
*
|
|
||||||
* @param mListener
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private InputStream parseLiteral() throws IOException {
|
|
||||||
expect('{');
|
|
||||||
int size = Integer.parseInt(readStringUntil('}'));
|
|
||||||
expect('\r');
|
|
||||||
expect('\n');
|
|
||||||
FixedLengthInputStream fixed = new FixedLengthInputStream(mIn, size);
|
|
||||||
return fixed;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A " has been read, read to the end of the quoted string and notify the
|
|
||||||
* listener.
|
|
||||||
*
|
|
||||||
* @param mListener
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private String parseQuoted() throws IOException {
|
|
||||||
expect('"');
|
|
||||||
return readStringUntil('"');
|
|
||||||
}
|
|
||||||
|
|
||||||
private String readStringUntil(char end) throws IOException {
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
int ch;
|
|
||||||
while ((ch = mIn.read()) != -1) {
|
|
||||||
if (ch == end) {
|
|
||||||
return sb.toString();
|
|
||||||
} else {
|
|
||||||
sb.append((char)ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw new IOException("readQuotedString(): end of stream reached");
|
|
||||||
}
|
|
||||||
|
|
||||||
private int expect(char ch) throws IOException {
|
|
||||||
int d;
|
|
||||||
if ((d = mIn.read()) != ch) {
|
|
||||||
throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)", (int)ch,
|
|
||||||
ch, d, (char)d));
|
|
||||||
}
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents an IMAP LIST response and is also the base class for the
|
|
||||||
* ImapResponse.
|
|
||||||
*/
|
|
||||||
public class ImapList extends ArrayList<Object> {
|
|
||||||
public ImapList getList(int index) {
|
|
||||||
return (ImapList)get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getString(int index) {
|
|
||||||
return (String)get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getLiteral(int index) {
|
|
||||||
return (InputStream)get(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getNumber(int index) {
|
|
||||||
return Integer.parseInt(getString(index));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getDate(int index) throws MessagingException {
|
|
||||||
try {
|
|
||||||
return mDateTimeFormat.parse(getString(index));
|
|
||||||
} catch (ParseException pe) {
|
|
||||||
throw new MessagingException("Unable to parse IMAP datetime", pe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Object getKeyedValue(Object key) {
|
|
||||||
for (int i = 0, count = size(); i < count; i++) {
|
|
||||||
if (get(i).equals(key)) {
|
|
||||||
return get(i + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImapList getKeyedList(Object key) {
|
|
||||||
return (ImapList)getKeyedValue(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getKeyedString(Object key) {
|
|
||||||
return (String)getKeyedValue(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public InputStream getKeyedLiteral(Object key) {
|
|
||||||
return (InputStream)getKeyedValue(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getKeyedNumber(Object key) {
|
|
||||||
return Integer.parseInt(getKeyedString(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
public Date getKeyedDate(Object key) throws MessagingException {
|
|
||||||
try {
|
|
||||||
String value = getKeyedString(key);
|
|
||||||
if (value == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return mDateTimeFormat.parse(value);
|
|
||||||
} catch (ParseException pe) {
|
|
||||||
throw new MessagingException("Unable to parse IMAP datetime", pe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a single response from the IMAP server. Tagged responses will
|
|
||||||
* have a non-null tag. Untagged responses will have a null tag. The object
|
|
||||||
* will contain all of the available tokens at the time the response is
|
|
||||||
* received. In general, it will either contain all of the tokens of the
|
|
||||||
* response or all of the tokens up until the first LITERAL. If the object
|
|
||||||
* does not contain the entire response the caller must call more() to
|
|
||||||
* continue reading the response until more returns false.
|
|
||||||
*/
|
|
||||||
public class ImapResponse extends ImapList {
|
|
||||||
private boolean mCompleted;
|
|
||||||
|
|
||||||
boolean mCommandContinuationRequested;
|
|
||||||
String mTag;
|
|
||||||
|
|
||||||
public boolean more() throws IOException {
|
|
||||||
if (mCompleted) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
readTokens(this);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getAlertText() {
|
|
||||||
if (size() > 1 && "[ALERT]".equals(getString(1))) {
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
for (int i = 2, count = size(); i < count; i++) {
|
|
||||||
sb.append(get(i).toString());
|
|
||||||
sb.append(' ');
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
} else {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return "#" + mTag + "# " + super.toString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,897 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail.store;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.net.SocketAddress;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
import javax.net.ssl.SSLException;
|
|
||||||
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.Utility;
|
|
||||||
import com.fsck.k9.mail.AuthenticationFailedException;
|
|
||||||
import com.fsck.k9.mail.FetchProfile;
|
|
||||||
import com.fsck.k9.mail.Flag;
|
|
||||||
import com.fsck.k9.mail.Folder;
|
|
||||||
import com.fsck.k9.mail.Message;
|
|
||||||
import com.fsck.k9.mail.MessageRetrievalListener;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
import com.fsck.k9.mail.Store;
|
|
||||||
import com.fsck.k9.mail.CertificateValidationException;
|
|
||||||
import com.fsck.k9.mail.Folder.OpenMode;
|
|
||||||
import com.fsck.k9.mail.internet.MimeMessage;
|
|
||||||
|
|
||||||
public class Pop3Store extends Store {
|
|
||||||
public static final int CONNECTION_SECURITY_NONE = 0;
|
|
||||||
public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
|
|
||||||
public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
|
|
||||||
public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
|
|
||||||
public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
|
|
||||||
|
|
||||||
private static final Flag[] PERMANENT_FLAGS = { Flag.DELETED };
|
|
||||||
|
|
||||||
private String mHost;
|
|
||||||
private int mPort;
|
|
||||||
private String mUsername;
|
|
||||||
private String mPassword;
|
|
||||||
private int mConnectionSecurity;
|
|
||||||
private HashMap<String, Folder> mFolders = new HashMap<String, Folder>();
|
|
||||||
private Pop3Capabilities mCapabilities;
|
|
||||||
|
|
||||||
// /**
|
|
||||||
// * Detected latency, used for usage scaling.
|
|
||||||
// * Usage scaling occurs when it is neccesary to get information about
|
|
||||||
// * messages that could result in large data loads. This value allows
|
|
||||||
// * the code that loads this data to decide between using large downloads
|
|
||||||
// * (high latency) or multiple round trips (low latency) to accomplish
|
|
||||||
// * the same thing.
|
|
||||||
// * Default is Integer.MAX_VALUE implying massive latency so that the large
|
|
||||||
// * download method is used by default until latency data is collected.
|
|
||||||
// */
|
|
||||||
// private int mLatencyMs = Integer.MAX_VALUE;
|
|
||||||
//
|
|
||||||
// /**
|
|
||||||
// * Detected throughput, used for usage scaling.
|
|
||||||
// * Usage scaling occurs when it is neccesary to get information about
|
|
||||||
// * messages that could result in large data loads. This value allows
|
|
||||||
// * the code that loads this data to decide between using large downloads
|
|
||||||
// * (high latency) or multiple round trips (low latency) to accomplish
|
|
||||||
// * the same thing.
|
|
||||||
// * Default is Integer.MAX_VALUE implying massive bandwidth so that the
|
|
||||||
// * large download method is used by default until latency data is
|
|
||||||
// * collected.
|
|
||||||
// */
|
|
||||||
// private int mThroughputKbS = Integer.MAX_VALUE;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* pop3://user:password@server:port CONNECTION_SECURITY_NONE
|
|
||||||
* pop3+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
|
|
||||||
* pop3+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
|
|
||||||
* pop3+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
|
|
||||||
* pop3+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
|
|
||||||
*
|
|
||||||
* @param _uri
|
|
||||||
*/
|
|
||||||
public Pop3Store(String _uri) throws MessagingException {
|
|
||||||
URI uri;
|
|
||||||
try {
|
|
||||||
uri = new URI(_uri);
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
throw new MessagingException("Invalid Pop3Store URI", use);
|
|
||||||
}
|
|
||||||
|
|
||||||
String scheme = uri.getScheme();
|
|
||||||
if (scheme.equals("pop3")) {
|
|
||||||
mConnectionSecurity = CONNECTION_SECURITY_NONE;
|
|
||||||
mPort = 110;
|
|
||||||
} else if (scheme.equals("pop3+tls")) {
|
|
||||||
mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
|
|
||||||
mPort = 110;
|
|
||||||
} else if (scheme.equals("pop3+tls+")) {
|
|
||||||
mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
|
|
||||||
mPort = 110;
|
|
||||||
} else if (scheme.equals("pop3+ssl+")) {
|
|
||||||
mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
|
|
||||||
mPort = 995;
|
|
||||||
} else if (scheme.equals("pop3+ssl")) {
|
|
||||||
mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
|
|
||||||
mPort = 995;
|
|
||||||
} else {
|
|
||||||
throw new MessagingException("Unsupported protocol");
|
|
||||||
}
|
|
||||||
|
|
||||||
mHost = uri.getHost();
|
|
||||||
|
|
||||||
if (uri.getPort() != -1) {
|
|
||||||
mPort = uri.getPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri.getUserInfo() != null) {
|
|
||||||
String[] userInfoParts = uri.getUserInfo().split(":", 2);
|
|
||||||
mUsername = userInfoParts[0];
|
|
||||||
if (userInfoParts.length > 1) {
|
|
||||||
mPassword = userInfoParts[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Folder getFolder(String name) throws MessagingException {
|
|
||||||
Folder folder = mFolders.get(name);
|
|
||||||
if (folder == null) {
|
|
||||||
folder = new Pop3Folder(name);
|
|
||||||
mFolders.put(folder.getName(), folder);
|
|
||||||
}
|
|
||||||
return folder;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Folder[] getPersonalNamespaces() throws MessagingException {
|
|
||||||
return new Folder[] {
|
|
||||||
getFolder("INBOX"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkSettings() throws MessagingException {
|
|
||||||
Pop3Folder folder = new Pop3Folder("INBOX");
|
|
||||||
folder.open(OpenMode.READ_WRITE);
|
|
||||||
if (!mCapabilities.uidl) {
|
|
||||||
/*
|
|
||||||
* Run an additional test to see if UIDL is supported on the server. If it's not we
|
|
||||||
* can't service this account.
|
|
||||||
*/
|
|
||||||
try{
|
|
||||||
/*
|
|
||||||
* If the server doesn't support UIDL it will return a - response, which causes
|
|
||||||
* executeSimpleCommand to throw a MessagingException, exiting this method.
|
|
||||||
*/
|
|
||||||
folder.executeSimpleCommand("UIDL");
|
|
||||||
}
|
|
||||||
catch (IOException ioe) {
|
|
||||||
throw new MessagingException(null, ioe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
folder.close(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
class Pop3Folder extends Folder {
|
|
||||||
private Socket mSocket;
|
|
||||||
private InputStream mIn;
|
|
||||||
private OutputStream mOut;
|
|
||||||
private HashMap<String, Pop3Message> mUidToMsgMap = new HashMap<String, Pop3Message>();
|
|
||||||
private HashMap<Integer, Pop3Message> mMsgNumToMsgMap = new HashMap<Integer, Pop3Message>();
|
|
||||||
private HashMap<String, Integer> mUidToMsgNumMap = new HashMap<String, Integer>();
|
|
||||||
private String mName;
|
|
||||||
private int mMessageCount;
|
|
||||||
|
|
||||||
public Pop3Folder(String name) {
|
|
||||||
this.mName = name;
|
|
||||||
if (mName.equalsIgnoreCase("INBOX")) {
|
|
||||||
mName = "INBOX";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public synchronized void open(OpenMode mode) throws MessagingException {
|
|
||||||
if (isOpen()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mName.equalsIgnoreCase("INBOX")) {
|
|
||||||
throw new MessagingException("Folder does not exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
|
|
||||||
if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED ||
|
|
||||||
mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) {
|
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
|
||||||
final boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED;
|
|
||||||
sslContext.init(null, new TrustManager[] {
|
|
||||||
TrustManagerFactory.get(mHost, secure)
|
|
||||||
}, new SecureRandom());
|
|
||||||
mSocket = sslContext.getSocketFactory().createSocket();
|
|
||||||
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
|
|
||||||
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
|
|
||||||
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
|
|
||||||
} else {
|
|
||||||
mSocket = new Socket();
|
|
||||||
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
|
|
||||||
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
|
|
||||||
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
|
|
||||||
}
|
|
||||||
|
|
||||||
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
|
|
||||||
|
|
||||||
|
|
||||||
// Eat the banner
|
|
||||||
executeSimpleCommand(null);
|
|
||||||
|
|
||||||
mCapabilities = getCapabilities();
|
|
||||||
|
|
||||||
if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL
|
|
||||||
|| mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
|
|
||||||
if (mCapabilities.stls) {
|
|
||||||
writeLine("STLS");
|
|
||||||
|
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
|
||||||
boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED;
|
|
||||||
sslContext.init(null, new TrustManager[] {
|
|
||||||
TrustManagerFactory.get(mHost, secure)
|
|
||||||
}, new SecureRandom());
|
|
||||||
mSocket = sslContext.getSocketFactory().createSocket(mSocket, mHost, mPort,
|
|
||||||
true);
|
|
||||||
mSocket.setSoTimeout(Store.SOCKET_READ_TIMEOUT);
|
|
||||||
mIn = new BufferedInputStream(mSocket.getInputStream(), 1024);
|
|
||||||
mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512);
|
|
||||||
} else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
|
|
||||||
throw new MessagingException("TLS not supported but required");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
executeSimpleCommand("USER " + mUsername);
|
|
||||||
executeSimpleCommand("PASS " + mPassword);
|
|
||||||
} catch (MessagingException me) {
|
|
||||||
throw new AuthenticationFailedException(null, me);
|
|
||||||
}
|
|
||||||
} catch (SSLException e) {
|
|
||||||
throw new CertificateValidationException(e.getMessage(), e);
|
|
||||||
} catch (GeneralSecurityException gse) {
|
|
||||||
throw new MessagingException(
|
|
||||||
"Unable to open connection to POP server due to security error.", gse);
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
throw new MessagingException("Unable to open connection to POP server.", ioe);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
String response = executeSimpleCommand("STAT");
|
|
||||||
String[] parts = response.split(" ");
|
|
||||||
mMessageCount = Integer.parseInt(parts[1]);
|
|
||||||
}
|
|
||||||
catch (IOException ioe) {
|
|
||||||
throw new MessagingException("Unable to STAT folder.", ioe);
|
|
||||||
}
|
|
||||||
mUidToMsgMap.clear();
|
|
||||||
mMsgNumToMsgMap.clear();
|
|
||||||
mUidToMsgNumMap.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isOpen() {
|
|
||||||
return (mIn != null && mOut != null && mSocket != null && mSocket.isConnected() && !mSocket
|
|
||||||
.isClosed());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public OpenMode getMode() throws MessagingException {
|
|
||||||
return OpenMode.READ_ONLY;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void close(boolean expunge) {
|
|
||||||
try {
|
|
||||||
executeSimpleCommand("QUIT");
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
/*
|
|
||||||
* QUIT may fail if the connection is already closed. We don't care. It's just
|
|
||||||
* being friendly.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
closeIO();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void closeIO() {
|
|
||||||
try {
|
|
||||||
mIn.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
/*
|
|
||||||
* May fail if the connection is already closed.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
mOut.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
/*
|
|
||||||
* May fail if the connection is already closed.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
mSocket.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
/*
|
|
||||||
* May fail if the connection is already closed.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
mIn = null;
|
|
||||||
mOut = null;
|
|
||||||
mSocket = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getName() {
|
|
||||||
return mName;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean create(FolderType type) throws MessagingException {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean exists() throws MessagingException {
|
|
||||||
return mName.equalsIgnoreCase("INBOX");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getMessageCount() {
|
|
||||||
return mMessageCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getUnreadMessageCount() throws MessagingException {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Message getMessage(String uid) throws MessagingException {
|
|
||||||
Pop3Message message = mUidToMsgMap.get(uid);
|
|
||||||
if (message == null) {
|
|
||||||
message = new Pop3Message(uid, this);
|
|
||||||
}
|
|
||||||
return message;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Message[] getMessages(int start, int end, MessageRetrievalListener listener)
|
|
||||||
throws MessagingException {
|
|
||||||
if (start < 1 || end < 1 || end < start) {
|
|
||||||
throw new MessagingException(String.format("Invalid message set %d %d",
|
|
||||||
start, end));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
indexMsgNums(start, end);
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
throw new MessagingException("getMessages", ioe);
|
|
||||||
}
|
|
||||||
ArrayList<Message> messages = new ArrayList<Message>();
|
|
||||||
int i = 0;
|
|
||||||
for (int msgNum = start; msgNum <= end; msgNum++) {
|
|
||||||
Pop3Message message = mMsgNumToMsgMap.get(msgNum);
|
|
||||||
if (listener != null) {
|
|
||||||
listener.messageStarted(message.getUid(), i++, (end - start) + 1);
|
|
||||||
}
|
|
||||||
messages.add(message);
|
|
||||||
if (listener != null) {
|
|
||||||
listener.messageFinished(message, i++, (end - start) + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return messages.toArray(new Message[messages.size()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Ensures that the given message set (from start to end inclusive)
|
|
||||||
* has been queried so that uids are available in the local cache.
|
|
||||||
* @param start
|
|
||||||
* @param end
|
|
||||||
* @throws MessagingException
|
|
||||||
* @throws IOException
|
|
||||||
*/
|
|
||||||
private void indexMsgNums(int start, int end)
|
|
||||||
throws MessagingException, IOException {
|
|
||||||
int unindexedMessageCount = 0;
|
|
||||||
for (int msgNum = start; msgNum <= end; msgNum++) {
|
|
||||||
if (mMsgNumToMsgMap.get(msgNum) == null) {
|
|
||||||
unindexedMessageCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (unindexedMessageCount == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (unindexedMessageCount < 50 && mMessageCount > 5000) {
|
|
||||||
/*
|
|
||||||
* In extreme cases we'll do a UIDL command per message instead of a bulk
|
|
||||||
* download.
|
|
||||||
*/
|
|
||||||
for (int msgNum = start; msgNum <= end; msgNum++) {
|
|
||||||
Pop3Message message = mMsgNumToMsgMap.get(msgNum);
|
|
||||||
if (message == null) {
|
|
||||||
String response = executeSimpleCommand("UIDL " + msgNum);
|
|
||||||
int uidIndex = response.lastIndexOf(' ');
|
|
||||||
String msgUid = response.substring(uidIndex + 1);
|
|
||||||
message = new Pop3Message(msgUid, this);
|
|
||||||
indexMessage(msgNum, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
String response = executeSimpleCommand("UIDL");
|
|
||||||
while ((response = readLine()) != null) {
|
|
||||||
if (response.equals(".")) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
String[] uidParts = response.split(" ");
|
|
||||||
Integer msgNum = Integer.valueOf(uidParts[0]);
|
|
||||||
String msgUid = uidParts[1];
|
|
||||||
if (msgNum >= start && msgNum <= end) {
|
|
||||||
Pop3Message message = mMsgNumToMsgMap.get(msgNum);
|
|
||||||
if (message == null) {
|
|
||||||
message = new Pop3Message(msgUid, this);
|
|
||||||
indexMessage(msgNum, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void indexUids(ArrayList<String> uids)
|
|
||||||
throws MessagingException, IOException {
|
|
||||||
HashSet<String> unindexedUids = new HashSet<String>();
|
|
||||||
for (String uid : uids) {
|
|
||||||
if (mUidToMsgMap.get(uid) == null) {
|
|
||||||
unindexedUids.add(uid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (unindexedUids.size() == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
/*
|
|
||||||
* If we are missing uids in the cache the only sure way to
|
|
||||||
* get them is to do a full UIDL list. A possible optimization
|
|
||||||
* would be trying UIDL for the latest X messages and praying.
|
|
||||||
*/
|
|
||||||
String response = executeSimpleCommand("UIDL");
|
|
||||||
while ((response = readLine()) != null) {
|
|
||||||
if (response.equals(".")) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
String[] uidParts = response.split(" ");
|
|
||||||
Integer msgNum = Integer.valueOf(uidParts[0]);
|
|
||||||
String msgUid = uidParts[1];
|
|
||||||
if (unindexedUids.contains(msgUid)) {
|
|
||||||
if (Config.LOGD) {
|
|
||||||
Pop3Message message = mUidToMsgMap.get(msgUid);
|
|
||||||
if (message == null) {
|
|
||||||
message = new Pop3Message(msgUid, this);
|
|
||||||
}
|
|
||||||
indexMessage(msgNum, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void indexMessage(int msgNum, Pop3Message message) {
|
|
||||||
mMsgNumToMsgMap.put(msgNum, message);
|
|
||||||
mUidToMsgMap.put(message.getUid(), message);
|
|
||||||
mUidToMsgNumMap.put(message.getUid(), msgNum);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Message[] getMessages(MessageRetrievalListener listener) throws MessagingException {
|
|
||||||
throw new UnsupportedOperationException("Pop3Folder.getMessage(MessageRetrievalListener)");
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Message[] getMessages(String[] uids, MessageRetrievalListener listener)
|
|
||||||
throws MessagingException {
|
|
||||||
throw new UnsupportedOperationException("Pop3Folder.getMessage(MessageRetrievalListener)");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch the items contained in the FetchProfile into the given set of
|
|
||||||
* Messages in as efficient a manner as possible.
|
|
||||||
* @param messages
|
|
||||||
* @param fp
|
|
||||||
* @throws MessagingException
|
|
||||||
*/
|
|
||||||
public void fetch(Message[] messages, FetchProfile fp, MessageRetrievalListener listener)
|
|
||||||
throws MessagingException {
|
|
||||||
if (messages == null || messages.length == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ArrayList<String> uids = new ArrayList<String>();
|
|
||||||
for (Message message : messages) {
|
|
||||||
uids.add(message.getUid());
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
indexUids(uids);
|
|
||||||
}
|
|
||||||
catch (IOException ioe) {
|
|
||||||
throw new MessagingException("fetch", ioe);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
if (fp.contains(FetchProfile.Item.ENVELOPE)) {
|
|
||||||
/*
|
|
||||||
* We pass the listener only if there are other things to do in the
|
|
||||||
* FetchProfile. Since fetchEnvelop works in bulk and eveything else
|
|
||||||
* works one at a time if we let fetchEnvelope send events the
|
|
||||||
* event would get sent twice.
|
|
||||||
*/
|
|
||||||
fetchEnvelope(messages, fp.size() == 1 ? listener : null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException ioe) {
|
|
||||||
throw new MessagingException("fetch", ioe);
|
|
||||||
}
|
|
||||||
for (int i = 0, count = messages.length; i < count; i++) {
|
|
||||||
Message message = messages[i];
|
|
||||||
if (!(message instanceof Pop3Message)) {
|
|
||||||
throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message");
|
|
||||||
}
|
|
||||||
Pop3Message pop3Message = (Pop3Message)message;
|
|
||||||
try {
|
|
||||||
if (listener != null && !fp.contains(FetchProfile.Item.ENVELOPE)) {
|
|
||||||
listener.messageStarted(pop3Message.getUid(), i, count);
|
|
||||||
}
|
|
||||||
if (fp.contains(FetchProfile.Item.BODY)) {
|
|
||||||
fetchBody(pop3Message, -1);
|
|
||||||
}
|
|
||||||
else if (fp.contains(FetchProfile.Item.BODY_SANE)) {
|
|
||||||
/*
|
|
||||||
* To convert the suggested download size we take the size
|
|
||||||
* divided by the maximum line size (76).
|
|
||||||
*/
|
|
||||||
fetchBody(pop3Message,
|
|
||||||
FETCH_BODY_SANE_SUGGESTED_SIZE / 76);
|
|
||||||
}
|
|
||||||
else if (fp.contains(FetchProfile.Item.STRUCTURE)) {
|
|
||||||
/*
|
|
||||||
* If the user is requesting STRUCTURE we are required to set the body
|
|
||||||
* to null since we do not support the function.
|
|
||||||
*/
|
|
||||||
pop3Message.setBody(null);
|
|
||||||
}
|
|
||||||
if (listener != null && !fp.contains(FetchProfile.Item.ENVELOPE)) {
|
|
||||||
listener.messageFinished(message, i, count);
|
|
||||||
}
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
throw new MessagingException("Unable to fetch message", ioe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void fetchEnvelope(Message[] messages,
|
|
||||||
MessageRetrievalListener listener) throws IOException, MessagingException {
|
|
||||||
int unsizedMessages = 0;
|
|
||||||
for (Message message : messages) {
|
|
||||||
if (message.getSize() == -1) {
|
|
||||||
unsizedMessages++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (unsizedMessages == 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (unsizedMessages < 50 && mMessageCount > 5000) {
|
|
||||||
/*
|
|
||||||
* In extreme cases we'll do a command per message instead of a bulk request
|
|
||||||
* to hopefully save some time and bandwidth.
|
|
||||||
*/
|
|
||||||
for (int i = 0, count = messages.length; i < count; i++) {
|
|
||||||
Message message = messages[i];
|
|
||||||
if (!(message instanceof Pop3Message)) {
|
|
||||||
throw new MessagingException("Pop3Store.fetch called with non-Pop3 Message");
|
|
||||||
}
|
|
||||||
Pop3Message pop3Message = (Pop3Message)message;
|
|
||||||
if (listener != null) {
|
|
||||||
listener.messageStarted(pop3Message.getUid(), i, count);
|
|
||||||
}
|
|
||||||
String response = executeSimpleCommand(String.format("LIST %d",
|
|
||||||
mUidToMsgNumMap.get(pop3Message.getUid())));
|
|
||||||
String[] listParts = response.split(" ");
|
|
||||||
int msgNum = Integer.parseInt(listParts[1]);
|
|
||||||
int msgSize = Integer.parseInt(listParts[2]);
|
|
||||||
pop3Message.setSize(msgSize);
|
|
||||||
if (listener != null) {
|
|
||||||
listener.messageFinished(pop3Message, i, count);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
HashSet<String> msgUidIndex = new HashSet<String>();
|
|
||||||
for (Message message : messages) {
|
|
||||||
msgUidIndex.add(message.getUid());
|
|
||||||
}
|
|
||||||
int i = 0, count = messages.length;
|
|
||||||
String response = executeSimpleCommand("LIST");
|
|
||||||
while ((response = readLine()) != null) {
|
|
||||||
if (response.equals(".")) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
String[] listParts = response.split(" ");
|
|
||||||
int msgNum = Integer.parseInt(listParts[0]);
|
|
||||||
int msgSize = Integer.parseInt(listParts[1]);
|
|
||||||
Pop3Message pop3Message = mMsgNumToMsgMap.get(msgNum);
|
|
||||||
if (pop3Message != null && msgUidIndex.contains(pop3Message.getUid())) {
|
|
||||||
if (listener != null) {
|
|
||||||
listener.messageStarted(pop3Message.getUid(), i, count);
|
|
||||||
}
|
|
||||||
pop3Message.setSize(msgSize);
|
|
||||||
if (listener != null) {
|
|
||||||
listener.messageFinished(pop3Message, i, count);
|
|
||||||
}
|
|
||||||
i++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches the body of the given message, limiting the stored data
|
|
||||||
* to the specified number of lines. If lines is -1 the entire message
|
|
||||||
* is fetched. This is implemented with RETR for lines = -1 or TOP
|
|
||||||
* for any other value. If the server does not support TOP it is
|
|
||||||
* emulated with RETR and extra lines are thrown away.
|
|
||||||
* @param message
|
|
||||||
* @param lines
|
|
||||||
*/
|
|
||||||
private void fetchBody(Pop3Message message, int lines)
|
|
||||||
throws IOException, MessagingException {
|
|
||||||
String response = null;
|
|
||||||
if (lines == -1 || !mCapabilities.top) {
|
|
||||||
response = executeSimpleCommand(String.format("RETR %d",
|
|
||||||
mUidToMsgNumMap.get(message.getUid())));
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
response = executeSimpleCommand(String.format("TOP %d %d",
|
|
||||||
mUidToMsgNumMap.get(message.getUid()),
|
|
||||||
lines));
|
|
||||||
}
|
|
||||||
if (response != null) {
|
|
||||||
try {
|
|
||||||
message.parse(new Pop3ResponseInputStream(mIn));
|
|
||||||
}
|
|
||||||
catch (MessagingException me) {
|
|
||||||
/*
|
|
||||||
* If we're only downloading headers it's possible
|
|
||||||
* we'll get a broken MIME message which we're not
|
|
||||||
* real worried about. If we've downloaded the body
|
|
||||||
* and can't parse it we need to let the user know.
|
|
||||||
*/
|
|
||||||
if (lines == -1) {
|
|
||||||
throw me;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Flag[] getPermanentFlags() throws MessagingException {
|
|
||||||
return PERMANENT_FLAGS;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void appendMessages(Message[] messages) throws MessagingException {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void delete(boolean recurse) throws MessagingException {
|
|
||||||
}
|
|
||||||
|
|
||||||
public Message[] expunge() throws MessagingException {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFlags(Message[] messages, Flag[] flags, boolean value)
|
|
||||||
throws MessagingException {
|
|
||||||
if (!value || !Utility.arrayContains(flags, Flag.DELETED)) {
|
|
||||||
/*
|
|
||||||
* The only flagging we support is setting the Deleted flag.
|
|
||||||
*/
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
for (Message message : messages) {
|
|
||||||
executeSimpleCommand(String.format("DELE %s",
|
|
||||||
mUidToMsgNumMap.get(message.getUid())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException ioe) {
|
|
||||||
throw new MessagingException("setFlags()", ioe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void copyMessages(Message[] msgs, Folder folder) throws MessagingException {
|
|
||||||
throw new UnsupportedOperationException("copyMessages is not supported in POP3");
|
|
||||||
}
|
|
||||||
|
|
||||||
// private boolean isRoundTripModeSuggested() {
|
|
||||||
// long roundTripMethodMs =
|
|
||||||
// (uncachedMessageCount * 2 * mLatencyMs);
|
|
||||||
// long bulkMethodMs =
|
|
||||||
// (mMessageCount * 58) / (mThroughputKbS * 1024 / 8) * 1000;
|
|
||||||
// }
|
|
||||||
|
|
||||||
private String readLine() throws IOException {
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
int d = mIn.read();
|
|
||||||
if (d == -1) {
|
|
||||||
throw new IOException("End of stream reached while trying to read line.");
|
|
||||||
}
|
|
||||||
do {
|
|
||||||
if (((char)d) == '\r') {
|
|
||||||
continue;
|
|
||||||
} else if (((char)d) == '\n') {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
sb.append((char)d);
|
|
||||||
}
|
|
||||||
} while ((d = mIn.read()) != -1);
|
|
||||||
String ret = sb.toString();
|
|
||||||
if (Config.LOGD) {
|
|
||||||
if (k9.DEBUG) {
|
|
||||||
Log.d(k9.LOG_TAG, "<<< " + ret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeLine(String s) throws IOException {
|
|
||||||
if (Config.LOGD) {
|
|
||||||
if (k9.DEBUG) {
|
|
||||||
Log.d(k9.LOG_TAG, ">>> " + s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mOut.write(s.getBytes());
|
|
||||||
mOut.write('\r');
|
|
||||||
mOut.write('\n');
|
|
||||||
mOut.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Pop3Capabilities getCapabilities() throws IOException, MessagingException {
|
|
||||||
Pop3Capabilities capabilities = new Pop3Capabilities();
|
|
||||||
try {
|
|
||||||
String response = executeSimpleCommand("CAPA");
|
|
||||||
while ((response = readLine()) != null) {
|
|
||||||
if (response.equals(".")) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (response.equalsIgnoreCase("STLS")){
|
|
||||||
capabilities.stls = true;
|
|
||||||
}
|
|
||||||
else if (response.equalsIgnoreCase("UIDL")) {
|
|
||||||
capabilities.uidl = true;
|
|
||||||
}
|
|
||||||
else if (response.equalsIgnoreCase("PIPELINING")) {
|
|
||||||
capabilities.pipelining = true;
|
|
||||||
}
|
|
||||||
else if (response.equalsIgnoreCase("USER")) {
|
|
||||||
capabilities.user = true;
|
|
||||||
}
|
|
||||||
else if (response.equalsIgnoreCase("TOP")) {
|
|
||||||
capabilities.top = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (MessagingException me) {
|
|
||||||
/*
|
|
||||||
* The server may not support the CAPA command, so we just eat this Exception
|
|
||||||
* and allow the empty capabilities object to be returned.
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
return capabilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String executeSimpleCommand(String command) throws IOException, MessagingException {
|
|
||||||
try {
|
|
||||||
open(OpenMode.READ_WRITE);
|
|
||||||
|
|
||||||
if (command != null) {
|
|
||||||
writeLine(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
String response = readLine();
|
|
||||||
|
|
||||||
if (response.length() > 1 && response.charAt(0) == '-') {
|
|
||||||
throw new MessagingException(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
closeIO();
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean supportsFetchingFlags() {
|
|
||||||
return false;
|
|
||||||
}//isFlagSupported
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean equals(Object o) {
|
|
||||||
if (o instanceof Pop3Folder) {
|
|
||||||
return ((Pop3Folder) o).mName.equals(mName);
|
|
||||||
}
|
|
||||||
return super.equals(o);
|
|
||||||
}
|
|
||||||
|
|
||||||
}//Pop3Folder
|
|
||||||
|
|
||||||
class Pop3Message extends MimeMessage {
|
|
||||||
public Pop3Message(String uid, Pop3Folder folder) throws MessagingException {
|
|
||||||
mUid = uid;
|
|
||||||
mFolder = folder;
|
|
||||||
mSize = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setSize(int size) {
|
|
||||||
mSize = size;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void parse(InputStream in) throws IOException, MessagingException {
|
|
||||||
super.parse(in);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void setFlag(Flag flag, boolean set) throws MessagingException {
|
|
||||||
super.setFlag(flag, set);
|
|
||||||
mFolder.setFlags(new Message[] { this }, new Flag[] { flag }, set);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Pop3Capabilities {
|
|
||||||
public boolean stls;
|
|
||||||
public boolean top;
|
|
||||||
public boolean user;
|
|
||||||
public boolean uidl;
|
|
||||||
public boolean pipelining;
|
|
||||||
|
|
||||||
public String toString() {
|
|
||||||
return String.format("STLS %b, TOP %b, USER %b, UIDL %b, PIPELINING %b",
|
|
||||||
stls,
|
|
||||||
top,
|
|
||||||
user,
|
|
||||||
uidl,
|
|
||||||
pipelining);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class Pop3ResponseInputStream extends InputStream {
|
|
||||||
InputStream mIn;
|
|
||||||
boolean mStartOfLine = true;
|
|
||||||
boolean mFinished;
|
|
||||||
|
|
||||||
public Pop3ResponseInputStream(InputStream in) {
|
|
||||||
mIn = in;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int read() throws IOException {
|
|
||||||
if (mFinished) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
int d = mIn.read();
|
|
||||||
if (mStartOfLine && d == '.') {
|
|
||||||
d = mIn.read();
|
|
||||||
if (d == '\r') {
|
|
||||||
mFinished = true;
|
|
||||||
mIn.read();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
mStartOfLine = (d == '\n');
|
|
||||||
|
|
||||||
return d;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,213 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail.store;
|
|
||||||
|
|
||||||
import android.util.Log;
|
|
||||||
import android.app.Application;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.net.http.DomainNameChecker;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.KeyStore;
|
|
||||||
import java.security.NoSuchAlgorithmException;
|
|
||||||
import java.security.KeyStoreException;
|
|
||||||
import java.security.cert.X509Certificate;
|
|
||||||
import java.security.cert.CertificateException;
|
|
||||||
|
|
||||||
import javax.net.ssl.X509TrustManager;
|
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
|
|
||||||
public final class TrustManagerFactory {
|
|
||||||
private static final String LOG_TAG = "TrustManagerFactory";
|
|
||||||
|
|
||||||
private static X509TrustManager defaultTrustManager;
|
|
||||||
private static X509TrustManager unsecureTrustManager;
|
|
||||||
private static X509TrustManager localTrustManager;
|
|
||||||
|
|
||||||
private static SecureX509TrustManager secureTrustManager;
|
|
||||||
|
|
||||||
private static X509Certificate[] lastCertChain = null;
|
|
||||||
|
|
||||||
private static File keyStoreFile;
|
|
||||||
private static KeyStore keyStore;
|
|
||||||
|
|
||||||
|
|
||||||
private static class SimpleX509TrustManager implements X509TrustManager {
|
|
||||||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
|
||||||
throws CertificateException {
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
|
||||||
throws CertificateException {
|
|
||||||
}
|
|
||||||
|
|
||||||
public X509Certificate[] getAcceptedIssuers() {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class SecureX509TrustManager implements X509TrustManager {
|
|
||||||
private static String mHost;
|
|
||||||
private static SecureX509TrustManager me;
|
|
||||||
|
|
||||||
private SecureX509TrustManager() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static X509TrustManager getInstance(String host) {
|
|
||||||
mHost = host;
|
|
||||||
if (me == null) {
|
|
||||||
me = new SecureX509TrustManager();
|
|
||||||
}
|
|
||||||
return me;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setHost(String host){
|
|
||||||
mHost = host;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkClientTrusted(X509Certificate[] chain, String authType)
|
|
||||||
throws CertificateException {
|
|
||||||
defaultTrustManager.checkClientTrusted(chain, authType);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void checkServerTrusted(X509Certificate[] chain, String authType)
|
|
||||||
throws CertificateException {
|
|
||||||
TrustManagerFactory.setLastCertChain(chain);
|
|
||||||
try {
|
|
||||||
defaultTrustManager.checkServerTrusted(chain, authType);
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
localTrustManager.checkServerTrusted(new X509Certificate[] {chain[0]}, authType);
|
|
||||||
}
|
|
||||||
if (!DomainNameChecker.match(chain[0], mHost)) {
|
|
||||||
try {
|
|
||||||
String dn = chain[0].getSubjectDN().toString();
|
|
||||||
if ((dn != null) && (dn.equalsIgnoreCase(keyStore.getCertificateAlias(chain[0])))) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
} catch (KeyStoreException e) {
|
|
||||||
throw new CertificateException("Certificate cannot be verified; KeyStore Exception: " + e);
|
|
||||||
}
|
|
||||||
throw new CertificateException("Certificate domain name does not match "
|
|
||||||
+ mHost);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public X509Certificate[] getAcceptedIssuers() {
|
|
||||||
return defaultTrustManager.getAcceptedIssuers();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
static {
|
|
||||||
try {
|
|
||||||
javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
|
|
||||||
Application app = k9.app;
|
|
||||||
keyStoreFile = new File(app.getDir("KeyStore", Context.MODE_PRIVATE) + File.separator + "KeyStore.bks");
|
|
||||||
keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
|
|
||||||
//TODO: read store from disk.
|
|
||||||
java.io.FileInputStream fis;
|
|
||||||
try {
|
|
||||||
fis = new java.io.FileInputStream(keyStoreFile);
|
|
||||||
} catch (FileNotFoundException e1) {
|
|
||||||
fis = null;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
keyStore.load(fis, "".toCharArray());
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.e(LOG_TAG, "KeyStore IOException while initializing TrustManagerFactory ", e);
|
|
||||||
keyStore = null;
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
Log.e(LOG_TAG, "KeyStore CertificateException while initializing TrustManagerFactory ", e);
|
|
||||||
keyStore = null;
|
|
||||||
}
|
|
||||||
tmf.init(keyStore);
|
|
||||||
TrustManager[] tms = tmf.getTrustManagers();
|
|
||||||
if (tms != null) {
|
|
||||||
for (TrustManager tm : tms) {
|
|
||||||
if (tm instanceof X509TrustManager) {
|
|
||||||
localTrustManager = (X509TrustManager)tm;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
|
|
||||||
tmf.init((KeyStore)null);
|
|
||||||
tms = tmf.getTrustManagers();
|
|
||||||
if (tms != null) {
|
|
||||||
for (TrustManager tm : tms) {
|
|
||||||
if (tm instanceof X509TrustManager) {
|
|
||||||
defaultTrustManager = (X509TrustManager) tm;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
Log.e(LOG_TAG, "Unable to get X509 Trust Manager ", e);
|
|
||||||
} catch (KeyStoreException e) {
|
|
||||||
Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
|
|
||||||
}
|
|
||||||
unsecureTrustManager = new SimpleX509TrustManager();
|
|
||||||
}
|
|
||||||
|
|
||||||
private TrustManagerFactory() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public static X509TrustManager get(String host, boolean secure) {
|
|
||||||
return secure ? SecureX509TrustManager.getInstance(host) :
|
|
||||||
unsecureTrustManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static KeyStore getKeyStore() {
|
|
||||||
return keyStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void setLastCertChain(X509Certificate[] chain) {
|
|
||||||
lastCertChain = chain;
|
|
||||||
}
|
|
||||||
public static X509Certificate[] getLastCertChain() {
|
|
||||||
return lastCertChain;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void addCertificateChain(String alias, X509Certificate[] chain) throws CertificateException {
|
|
||||||
try {
|
|
||||||
javax.net.ssl.TrustManagerFactory tmf = javax.net.ssl.TrustManagerFactory.getInstance("X509");
|
|
||||||
for (int i = 0; i < chain.length; i++)
|
|
||||||
{
|
|
||||||
keyStore.setCertificateEntry
|
|
||||||
(chain[i].getSubjectDN().toString(), chain[i]);
|
|
||||||
}
|
|
||||||
|
|
||||||
tmf.init(keyStore);
|
|
||||||
TrustManager[] tms = tmf.getTrustManagers();
|
|
||||||
if (tms != null) {
|
|
||||||
for (TrustManager tm : tms) {
|
|
||||||
if (tm instanceof X509TrustManager) {
|
|
||||||
localTrustManager = (X509TrustManager) tm;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
java.io.FileOutputStream keyStoreStream;
|
|
||||||
try {
|
|
||||||
keyStoreStream = new java.io.FileOutputStream(keyStoreFile);
|
|
||||||
keyStore.store(keyStoreStream, "".toCharArray());
|
|
||||||
keyStoreStream.close();
|
|
||||||
} catch (FileNotFoundException e) {
|
|
||||||
throw new CertificateException("Unable to write KeyStore: " + e.getMessage());
|
|
||||||
} catch (CertificateException e) {
|
|
||||||
throw new CertificateException("Unable to write KeyStore: " + e.getMessage());
|
|
||||||
} catch (IOException e) {
|
|
||||||
throw new CertificateException("Unable to write KeyStore: " + e.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (NoSuchAlgorithmException e) {
|
|
||||||
Log.e(LOG_TAG, "Unable to get X509 Trust Manager ", e);
|
|
||||||
} catch (KeyStoreException e) {
|
|
||||||
Log.e(LOG_TAG, "Key Store exception while initializing TrustManagerFactory ", e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -1,24 +0,0 @@
|
|||||||
package com.fsck.k9.mail.transport;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple OutputStream that does nothing but count how many bytes are written to it and
|
|
||||||
* makes that count available to callers.
|
|
||||||
*/
|
|
||||||
public class CountingOutputStream extends OutputStream {
|
|
||||||
private long mCount;
|
|
||||||
|
|
||||||
public CountingOutputStream() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public long getCount() {
|
|
||||||
return mCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(int oneByte) throws IOException {
|
|
||||||
mCount++;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package com.fsck.k9.mail.transport;
|
|
||||||
|
|
||||||
import java.io.FilterOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
public class EOLConvertingOutputStream extends FilterOutputStream {
|
|
||||||
int lastChar;
|
|
||||||
|
|
||||||
public EOLConvertingOutputStream(OutputStream out) {
|
|
||||||
super(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(int oneByte) throws IOException {
|
|
||||||
if (oneByte == '\n') {
|
|
||||||
if (lastChar != '\r') {
|
|
||||||
super.write('\r');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
super.write(oneByte);
|
|
||||||
lastChar = oneByte;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void flush() throws IOException {
|
|
||||||
if (lastChar == '\r') {
|
|
||||||
super.write('\n');
|
|
||||||
lastChar = '\n';
|
|
||||||
}
|
|
||||||
super.flush();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,376 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail.transport;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.net.SocketAddress;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
import javax.net.ssl.SSLException;
|
|
||||||
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.PeekableInputStream;
|
|
||||||
import com.fsck.k9.codec.binary.Base64;
|
|
||||||
import com.fsck.k9.mail.Address;
|
|
||||||
import com.fsck.k9.mail.AuthenticationFailedException;
|
|
||||||
import com.fsck.k9.mail.Message;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
import com.fsck.k9.mail.Transport;
|
|
||||||
import com.fsck.k9.mail.CertificateValidationException;
|
|
||||||
import com.fsck.k9.mail.Message.RecipientType;
|
|
||||||
import com.fsck.k9.mail.store.TrustManagerFactory;
|
|
||||||
|
|
||||||
public class SmtpTransport extends Transport {
|
|
||||||
public static final int CONNECTION_SECURITY_NONE = 0;
|
|
||||||
|
|
||||||
public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
|
|
||||||
|
|
||||||
public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
|
|
||||||
|
|
||||||
public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
|
|
||||||
|
|
||||||
public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
|
|
||||||
|
|
||||||
String mHost;
|
|
||||||
|
|
||||||
int mPort;
|
|
||||||
|
|
||||||
String mUsername;
|
|
||||||
|
|
||||||
String mPassword;
|
|
||||||
|
|
||||||
int mConnectionSecurity;
|
|
||||||
|
|
||||||
boolean mSecure;
|
|
||||||
|
|
||||||
Socket mSocket;
|
|
||||||
|
|
||||||
PeekableInputStream mIn;
|
|
||||||
|
|
||||||
OutputStream mOut;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* smtp://user:password@server:port CONNECTION_SECURITY_NONE
|
|
||||||
* smtp+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
|
|
||||||
* smtp+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
|
|
||||||
* smtp+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
|
|
||||||
* smtp+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
|
|
||||||
*
|
|
||||||
* @param _uri
|
|
||||||
*/
|
|
||||||
public SmtpTransport(String _uri) throws MessagingException {
|
|
||||||
URI uri;
|
|
||||||
try {
|
|
||||||
uri = new URI(_uri);
|
|
||||||
} catch (URISyntaxException use) {
|
|
||||||
throw new MessagingException("Invalid SmtpTransport URI", use);
|
|
||||||
}
|
|
||||||
|
|
||||||
String scheme = uri.getScheme();
|
|
||||||
if (scheme.equals("smtp")) {
|
|
||||||
mConnectionSecurity = CONNECTION_SECURITY_NONE;
|
|
||||||
mPort = 25;
|
|
||||||
} else if (scheme.equals("smtp+tls")) {
|
|
||||||
mConnectionSecurity = CONNECTION_SECURITY_TLS_OPTIONAL;
|
|
||||||
mPort = 25;
|
|
||||||
} else if (scheme.equals("smtp+tls+")) {
|
|
||||||
mConnectionSecurity = CONNECTION_SECURITY_TLS_REQUIRED;
|
|
||||||
mPort = 25;
|
|
||||||
} else if (scheme.equals("smtp+ssl+")) {
|
|
||||||
mConnectionSecurity = CONNECTION_SECURITY_SSL_REQUIRED;
|
|
||||||
mPort = 465;
|
|
||||||
} else if (scheme.equals("smtp+ssl")) {
|
|
||||||
mConnectionSecurity = CONNECTION_SECURITY_SSL_OPTIONAL;
|
|
||||||
mPort = 465;
|
|
||||||
} else {
|
|
||||||
throw new MessagingException("Unsupported protocol");
|
|
||||||
}
|
|
||||||
|
|
||||||
mHost = uri.getHost();
|
|
||||||
|
|
||||||
if (uri.getPort() != -1) {
|
|
||||||
mPort = uri.getPort();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (uri.getUserInfo() != null) {
|
|
||||||
String[] userInfoParts = uri.getUserInfo().split(":", 2);
|
|
||||||
mUsername = userInfoParts[0];
|
|
||||||
if (userInfoParts.length > 1) {
|
|
||||||
mPassword = userInfoParts[1];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void open() throws MessagingException {
|
|
||||||
try {
|
|
||||||
SocketAddress socketAddress = new InetSocketAddress(mHost, mPort);
|
|
||||||
if (mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED ||
|
|
||||||
mConnectionSecurity == CONNECTION_SECURITY_SSL_OPTIONAL) {
|
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
|
||||||
boolean secure = mConnectionSecurity == CONNECTION_SECURITY_SSL_REQUIRED;
|
|
||||||
sslContext.init(null, new TrustManager[] {
|
|
||||||
TrustManagerFactory.get(mHost, secure)
|
|
||||||
}, new SecureRandom());
|
|
||||||
mSocket = sslContext.getSocketFactory().createSocket();
|
|
||||||
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
|
|
||||||
mSecure = true;
|
|
||||||
} else {
|
|
||||||
mSocket = new Socket();
|
|
||||||
mSocket.connect(socketAddress, SOCKET_CONNECT_TIMEOUT);
|
|
||||||
}
|
|
||||||
|
|
||||||
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(), 1024));
|
|
||||||
mOut = mSocket.getOutputStream();
|
|
||||||
|
|
||||||
// Eat the banner
|
|
||||||
executeSimpleCommand(null);
|
|
||||||
|
|
||||||
String localHost = "localhost.localdomain";
|
|
||||||
try {
|
|
||||||
InetAddress localAddress = InetAddress.getLocalHost();
|
|
||||||
if (! localAddress.isLoopbackAddress()) {
|
|
||||||
// The loopback address will resolve to 'localhost'
|
|
||||||
// some mail servers only accept qualified hostnames, so make sure
|
|
||||||
// never to override "localhost.localdomain" with "localhost"
|
|
||||||
// TODO - this is a hack. but a better hack than what was there before
|
|
||||||
localHost = localAddress.getHostName();
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
if (Config.LOGD) {
|
|
||||||
if (k9.DEBUG) {
|
|
||||||
Log.d(k9.LOG_TAG, "Unable to look up localhost");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String result = executeSimpleCommand("EHLO " + localHost);
|
|
||||||
|
|
||||||
/*
|
|
||||||
* TODO may need to add code to fall back to HELO I switched it from
|
|
||||||
* using HELO on non STARTTLS connections because of AOL's mail
|
|
||||||
* server. It won't let you use AUTH without EHLO.
|
|
||||||
* We should really be paying more attention to the capabilities
|
|
||||||
* and only attempting auth if it's available, and warning the user
|
|
||||||
* if not.
|
|
||||||
*/
|
|
||||||
if (mConnectionSecurity == CONNECTION_SECURITY_TLS_OPTIONAL
|
|
||||||
|| mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
|
|
||||||
if (result.contains("-STARTTLS")) {
|
|
||||||
executeSimpleCommand("STARTTLS");
|
|
||||||
|
|
||||||
SSLContext sslContext = SSLContext.getInstance("TLS");
|
|
||||||
boolean secure = mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED;
|
|
||||||
sslContext.init(null, new TrustManager[] {
|
|
||||||
TrustManagerFactory.get(mHost, secure)
|
|
||||||
}, new SecureRandom());
|
|
||||||
mSocket = sslContext.getSocketFactory().createSocket(mSocket, mHost, mPort,
|
|
||||||
true);
|
|
||||||
mIn = new PeekableInputStream(new BufferedInputStream(mSocket.getInputStream(),
|
|
||||||
1024));
|
|
||||||
mOut = mSocket.getOutputStream();
|
|
||||||
mSecure = true;
|
|
||||||
/*
|
|
||||||
* Now resend the EHLO. Required by RFC2487 Sec. 5.2, and more specifically,
|
|
||||||
* Exim.
|
|
||||||
*/
|
|
||||||
result = executeSimpleCommand("EHLO " + localHost);
|
|
||||||
} else if (mConnectionSecurity == CONNECTION_SECURITY_TLS_REQUIRED) {
|
|
||||||
throw new MessagingException("TLS not supported but required");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
* result contains the results of the EHLO in concatenated form
|
|
||||||
*/
|
|
||||||
boolean authLoginSupported = result.matches(".*AUTH.*LOGIN.*$");
|
|
||||||
boolean authPlainSupported = result.matches(".*AUTH.*PLAIN.*$");
|
|
||||||
|
|
||||||
if (mUsername != null && mUsername.length() > 0 && mPassword != null
|
|
||||||
&& mPassword.length() > 0) {
|
|
||||||
if (authPlainSupported) {
|
|
||||||
saslAuthPlain(mUsername, mPassword);
|
|
||||||
}
|
|
||||||
else if (authLoginSupported) {
|
|
||||||
saslAuthLogin(mUsername, mPassword);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new MessagingException("No valid authentication mechanism found.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (SSLException e) {
|
|
||||||
throw new CertificateValidationException(e.getMessage(), e);
|
|
||||||
} catch (GeneralSecurityException gse) {
|
|
||||||
throw new MessagingException(
|
|
||||||
"Unable to open connection to SMTP server due to security error.", gse);
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
throw new MessagingException("Unable to open connection to SMTP server.", ioe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendMessage(Message message) throws MessagingException {
|
|
||||||
close();
|
|
||||||
open();
|
|
||||||
Address[] from = message.getFrom();
|
|
||||||
|
|
||||||
try {
|
|
||||||
executeSimpleCommand("MAIL FROM: " + "<" + from[0].getAddress() + ">");
|
|
||||||
for (Address address : message.getRecipients(RecipientType.TO)) {
|
|
||||||
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
|
|
||||||
}
|
|
||||||
for (Address address : message.getRecipients(RecipientType.CC)) {
|
|
||||||
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
|
|
||||||
}
|
|
||||||
for (Address address : message.getRecipients(RecipientType.BCC)) {
|
|
||||||
executeSimpleCommand("RCPT TO: " + "<" + address.getAddress() + ">");
|
|
||||||
}
|
|
||||||
message.setRecipients(RecipientType.BCC, null);
|
|
||||||
executeSimpleCommand("DATA");
|
|
||||||
// TODO byte stuffing
|
|
||||||
message.writeTo(
|
|
||||||
new EOLConvertingOutputStream(
|
|
||||||
new BufferedOutputStream(mOut, 1024)));
|
|
||||||
executeSimpleCommand("\r\n.");
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
throw new MessagingException("Unable to send message", ioe);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
try {
|
|
||||||
mIn.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
mOut.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
mSocket.close();
|
|
||||||
} catch (Exception e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
mIn = null;
|
|
||||||
mOut = null;
|
|
||||||
mSocket = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private String readLine() throws IOException {
|
|
||||||
StringBuffer sb = new StringBuffer();
|
|
||||||
int d;
|
|
||||||
while ((d = mIn.read()) != -1) {
|
|
||||||
if (((char)d) == '\r') {
|
|
||||||
continue;
|
|
||||||
} else if (((char)d) == '\n') {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
sb.append((char)d);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String ret = sb.toString();
|
|
||||||
if (Config.LOGD) {
|
|
||||||
if (k9.DEBUG) {
|
|
||||||
Log.d(k9.LOG_TAG, "<<< " + ret);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeLine(String s) throws IOException {
|
|
||||||
if (Config.LOGD) {
|
|
||||||
if (k9.DEBUG) {
|
|
||||||
Log.d(k9.LOG_TAG, ">>> " + s);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mOut.write(s.getBytes());
|
|
||||||
mOut.write('\r');
|
|
||||||
mOut.write('\n');
|
|
||||||
mOut.flush();
|
|
||||||
}
|
|
||||||
|
|
||||||
private String executeSimpleCommand(String command) throws IOException, MessagingException {
|
|
||||||
if (command != null) {
|
|
||||||
writeLine(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
String line = readLine();
|
|
||||||
|
|
||||||
String result = line;
|
|
||||||
|
|
||||||
while (line.length() >= 4 && line.charAt(3) == '-') {
|
|
||||||
line = readLine();
|
|
||||||
result += line.substring(3);
|
|
||||||
}
|
|
||||||
|
|
||||||
char c = result.charAt(0);
|
|
||||||
if ((c == '4') || (c == '5')) {
|
|
||||||
throw new MessagingException(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// C: AUTH LOGIN
|
|
||||||
// S: 334 VXNlcm5hbWU6
|
|
||||||
// C: d2VsZG9u
|
|
||||||
// S: 334 UGFzc3dvcmQ6
|
|
||||||
// C: dzNsZDBu
|
|
||||||
// S: 235 2.0.0 OK Authenticated
|
|
||||||
//
|
|
||||||
// Lines 2-5 of the conversation contain base64-encoded information. The same conversation, with base64 strings decoded, reads:
|
|
||||||
//
|
|
||||||
//
|
|
||||||
// C: AUTH LOGIN
|
|
||||||
// S: 334 Username:
|
|
||||||
// C: weldon
|
|
||||||
// S: 334 Password:
|
|
||||||
// C: w3ld0n
|
|
||||||
// S: 235 2.0.0 OK Authenticated
|
|
||||||
|
|
||||||
private void saslAuthLogin(String username, String password) throws MessagingException,
|
|
||||||
AuthenticationFailedException, IOException {
|
|
||||||
try {
|
|
||||||
executeSimpleCommand("AUTH LOGIN");
|
|
||||||
executeSimpleCommand(new String(Base64.encodeBase64(username.getBytes())));
|
|
||||||
executeSimpleCommand(new String(Base64.encodeBase64(password.getBytes())));
|
|
||||||
}
|
|
||||||
catch (MessagingException me) {
|
|
||||||
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
|
|
||||||
throw new AuthenticationFailedException("AUTH LOGIN failed (" + me.getMessage()
|
|
||||||
+ ")");
|
|
||||||
}
|
|
||||||
throw me;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void saslAuthPlain(String username, String password) throws MessagingException,
|
|
||||||
AuthenticationFailedException, IOException {
|
|
||||||
byte[] data = ("\000" + username + "\000" + password).getBytes();
|
|
||||||
data = new Base64().encode(data);
|
|
||||||
try {
|
|
||||||
executeSimpleCommand("AUTH PLAIN " + new String(data));
|
|
||||||
}
|
|
||||||
catch (MessagingException me) {
|
|
||||||
if (me.getMessage().length() > 1 && me.getMessage().charAt(1) == '3') {
|
|
||||||
throw new AuthenticationFailedException("AUTH PLAIN failed (" + me.getMessage()
|
|
||||||
+ ")");
|
|
||||||
}
|
|
||||||
throw me;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,29 +0,0 @@
|
|||||||
package com.fsck.k9.mail.transport;
|
|
||||||
|
|
||||||
import java.io.FilterOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
public class StatusOutputStream extends FilterOutputStream {
|
|
||||||
private long mCount = 0;
|
|
||||||
|
|
||||||
public StatusOutputStream(OutputStream out) {
|
|
||||||
super(out);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(int oneByte) throws IOException {
|
|
||||||
super.write(oneByte);
|
|
||||||
mCount++;
|
|
||||||
if (Config.LOGV) {
|
|
||||||
if (mCount % 1024 == 0) {
|
|
||||||
Log.v(k9.LOG_TAG, "# " + mCount);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,189 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.mail.transport;
|
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
|
||||||
import java.io.BufferedOutputStream;
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.UnsupportedEncodingException;
|
|
||||||
import java.net.InetAddress;
|
|
||||||
import java.net.InetSocketAddress;
|
|
||||||
import java.net.Socket;
|
|
||||||
import java.net.SocketAddress;
|
|
||||||
import java.net.URI;
|
|
||||||
import java.net.URISyntaxException;
|
|
||||||
import java.net.URLEncoder;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.security.SecureRandom;
|
|
||||||
|
|
||||||
import javax.net.ssl.SSLContext;
|
|
||||||
import javax.net.ssl.TrustManager;
|
|
||||||
import javax.net.ssl.SSLException;
|
|
||||||
import javax.xml.parsers.ParserConfigurationException;
|
|
||||||
import javax.xml.parsers.SAXParser;
|
|
||||||
import javax.xml.parsers.SAXParserFactory;
|
|
||||||
|
|
||||||
import org.apache.http.HttpEntity;
|
|
||||||
import org.apache.http.HttpResponse;
|
|
||||||
import org.apache.http.client.CookieStore;
|
|
||||||
import org.apache.http.entity.StringEntity;
|
|
||||||
import org.apache.http.impl.client.DefaultHttpClient;
|
|
||||||
import org.xml.sax.InputSource;
|
|
||||||
import org.xml.sax.SAXException;
|
|
||||||
import org.xml.sax.XMLReader;
|
|
||||||
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.PeekableInputStream;
|
|
||||||
import com.fsck.k9.codec.binary.Base64;
|
|
||||||
import com.fsck.k9.mail.Address;
|
|
||||||
import com.fsck.k9.mail.AuthenticationFailedException;
|
|
||||||
import com.fsck.k9.mail.Folder;
|
|
||||||
import com.fsck.k9.mail.Message;
|
|
||||||
import com.fsck.k9.mail.MessagingException;
|
|
||||||
import com.fsck.k9.mail.Transport;
|
|
||||||
import com.fsck.k9.mail.CertificateValidationException;
|
|
||||||
import com.fsck.k9.mail.Message.RecipientType;
|
|
||||||
import com.fsck.k9.mail.store.TrustManagerFactory;
|
|
||||||
import com.fsck.k9.mail.store.WebDavStore;
|
|
||||||
import com.fsck.k9.mail.store.WebDavStore.HttpGeneric;
|
|
||||||
import com.fsck.k9.mail.store.WebDavStore.ParsedDataSet;
|
|
||||||
import com.fsck.k9.mail.store.WebDavStore.WebDavHandler;
|
|
||||||
|
|
||||||
public class WebDavTransport extends Transport {
|
|
||||||
public static final int CONNECTION_SECURITY_NONE = 0;
|
|
||||||
public static final int CONNECTION_SECURITY_TLS_OPTIONAL = 1;
|
|
||||||
public static final int CONNECTION_SECURITY_TLS_REQUIRED = 2;
|
|
||||||
public static final int CONNECTION_SECURITY_SSL_REQUIRED = 3;
|
|
||||||
public static final int CONNECTION_SECURITY_SSL_OPTIONAL = 4;
|
|
||||||
|
|
||||||
String host;
|
|
||||||
int mPort;
|
|
||||||
private int mConnectionSecurity;
|
|
||||||
private String mUsername; /* Stores the username for authentications */
|
|
||||||
private String mPassword; /* Stores the password for authentications */
|
|
||||||
private String mUrl; /* Stores the base URL for the server */
|
|
||||||
|
|
||||||
boolean mSecure;
|
|
||||||
Socket mSocket;
|
|
||||||
PeekableInputStream mIn;
|
|
||||||
OutputStream mOut;
|
|
||||||
private WebDavStore store;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* webdav://user:password@server:port CONNECTION_SECURITY_NONE
|
|
||||||
* webdav+tls://user:password@server:port CONNECTION_SECURITY_TLS_OPTIONAL
|
|
||||||
* webdav+tls+://user:password@server:port CONNECTION_SECURITY_TLS_REQUIRED
|
|
||||||
* webdav+ssl+://user:password@server:port CONNECTION_SECURITY_SSL_REQUIRED
|
|
||||||
* webdav+ssl://user:password@server:port CONNECTION_SECURITY_SSL_OPTIONAL
|
|
||||||
*
|
|
||||||
* @param _uri
|
|
||||||
*/
|
|
||||||
public WebDavTransport(String _uri) throws MessagingException {
|
|
||||||
store = new WebDavStore(_uri);
|
|
||||||
Log.d(k9.LOG_TAG, ">>> New WebDavTransport creation complete");
|
|
||||||
}
|
|
||||||
|
|
||||||
public void open() throws MessagingException {
|
|
||||||
Log.d(k9.LOG_TAG, ">>> open called on WebDavTransport ");
|
|
||||||
if (store.needAuth()) {
|
|
||||||
store.authenticate();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (store.getAuthCookies() == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// public void sendMessage(Message message) throws MessagingException {
|
|
||||||
// Address[] from = message.getFrom();
|
|
||||||
//
|
|
||||||
// }
|
|
||||||
|
|
||||||
public void close() {
|
|
||||||
}
|
|
||||||
|
|
||||||
public String generateTempURI(String subject) {
|
|
||||||
String encodedSubject = URLEncoder.encode(subject);
|
|
||||||
return store.getUrl() + "/Exchange/" + store.getAlias() + "/drafts/" + encodedSubject + ".eml";
|
|
||||||
}
|
|
||||||
public String generateSendURI() {
|
|
||||||
return store.getUrl() + "/Exchange/" + store.getAlias() + "/##DavMailSubmissionURI##/";
|
|
||||||
}
|
|
||||||
|
|
||||||
public void sendMessage(Message message) throws MessagingException {
|
|
||||||
Log.d(k9.LOG_TAG, ">>> sendMessage called.");
|
|
||||||
|
|
||||||
DefaultHttpClient httpclient = new DefaultHttpClient();
|
|
||||||
HttpGeneric httpmethod;
|
|
||||||
HttpResponse response;
|
|
||||||
HttpEntity responseEntity;
|
|
||||||
StringEntity bodyEntity;
|
|
||||||
int statusCode;
|
|
||||||
String subject;
|
|
||||||
ByteArrayOutputStream out;
|
|
||||||
try {
|
|
||||||
try {
|
|
||||||
subject = message.getSubject();
|
|
||||||
} catch (MessagingException e) {
|
|
||||||
Log.e(k9.LOG_TAG, "MessagingException while retrieving Subject: " + e);
|
|
||||||
subject = "";
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
out = new ByteArrayOutputStream(message.getSize());
|
|
||||||
} catch (MessagingException e) {
|
|
||||||
Log.e(k9.LOG_TAG, "MessagingException while getting size of message: " + e);
|
|
||||||
out = new ByteArrayOutputStream();
|
|
||||||
}
|
|
||||||
open();
|
|
||||||
message.writeTo(
|
|
||||||
new EOLConvertingOutputStream(
|
|
||||||
new BufferedOutputStream(out, 1024)));
|
|
||||||
httpclient.setCookieStore(store.getAuthCookies());
|
|
||||||
|
|
||||||
bodyEntity = new StringEntity(out.toString(), "UTF-8");
|
|
||||||
bodyEntity.setContentType("message/rfc822");
|
|
||||||
|
|
||||||
httpmethod = store.new HttpGeneric(generateTempURI(subject));
|
|
||||||
httpmethod.setMethod("PUT");
|
|
||||||
httpmethod.setEntity(bodyEntity);
|
|
||||||
|
|
||||||
response = httpclient.execute(httpmethod);
|
|
||||||
statusCode = response.getStatusLine().getStatusCode();
|
|
||||||
|
|
||||||
if (statusCode < 200 ||
|
|
||||||
statusCode > 300) {
|
|
||||||
throw new IOException("Error sending message, status code was " + statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
//responseEntity = response.getEntity();
|
|
||||||
//DefaultHttpClient movehttpclient = new DefaultHttpClient();
|
|
||||||
//HttpGeneric movehttpmethod;
|
|
||||||
//HttpResponse moveresponse;
|
|
||||||
//HttpEntity moveresponseEntity;
|
|
||||||
httpmethod = store.new HttpGeneric(generateTempURI(subject));
|
|
||||||
httpmethod.setMethod("MOVE");
|
|
||||||
httpmethod.setHeader("Destination", generateSendURI());
|
|
||||||
|
|
||||||
response = httpclient.execute(httpmethod);
|
|
||||||
statusCode = response.getStatusLine().getStatusCode();
|
|
||||||
|
|
||||||
if (statusCode < 200 ||
|
|
||||||
statusCode > 300) {
|
|
||||||
throw new IOException("Error sending message, status code was " + statusCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (UnsupportedEncodingException uee) {
|
|
||||||
Log.e(k9.LOG_TAG, "UnsupportedEncodingException in getMessageCount() " + uee);
|
|
||||||
} catch (IOException ioe) {
|
|
||||||
Log.e(k9.LOG_TAG, "IOException in getMessageCount() " + ioe);
|
|
||||||
throw new MessagingException("Unable to send message", ioe);
|
|
||||||
}
|
|
||||||
Log.d(k9.LOG_TAG, ">>> getMessageCount finished");
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,272 +0,0 @@
|
|||||||
package com.fsck.k9.provider;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileInputStream;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import android.content.ContentProvider;
|
|
||||||
import android.content.ContentValues;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.database.MatrixCursor;
|
|
||||||
import android.database.sqlite.SQLiteDatabase;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.ParcelFileDescriptor;
|
|
||||||
import android.provider.OpenableColumns;
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.Utility;
|
|
||||||
import com.fsck.k9.mail.internet.MimeUtility;
|
|
||||||
|
|
||||||
/*
|
|
||||||
* A simple ContentProvider that allows file access to Email's attachments.
|
|
||||||
*/
|
|
||||||
public class AttachmentProvider extends ContentProvider {
|
|
||||||
public static final Uri CONTENT_URI = Uri.parse( "content://com.fsck.k9.attachmentprovider");
|
|
||||||
|
|
||||||
private static final String FORMAT_RAW = "RAW";
|
|
||||||
private static final String FORMAT_THUMBNAIL = "THUMBNAIL";
|
|
||||||
|
|
||||||
public static class AttachmentProviderColumns {
|
|
||||||
public static final String _ID = "_id";
|
|
||||||
public static final String DATA = "_data";
|
|
||||||
public static final String DISPLAY_NAME = "_display_name";
|
|
||||||
public static final String SIZE = "_size";
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Uri getAttachmentUri(Account account, long id) {
|
|
||||||
return CONTENT_URI.buildUpon()
|
|
||||||
.appendPath(account.getUuid() + ".db")
|
|
||||||
.appendPath(Long.toString(id))
|
|
||||||
.appendPath(FORMAT_RAW)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Uri getAttachmentThumbnailUri(Account account, long id, int width, int height) {
|
|
||||||
return CONTENT_URI.buildUpon()
|
|
||||||
.appendPath(account.getUuid() + ".db")
|
|
||||||
.appendPath(Long.toString(id))
|
|
||||||
.appendPath(FORMAT_THUMBNAIL)
|
|
||||||
.appendPath(Integer.toString(width))
|
|
||||||
.appendPath(Integer.toString(height))
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
public static Uri getAttachmentUri(String db, long id) {
|
|
||||||
return CONTENT_URI.buildUpon()
|
|
||||||
.appendPath(db)
|
|
||||||
.appendPath(Long.toString(id))
|
|
||||||
.appendPath(FORMAT_RAW)
|
|
||||||
.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onCreate() {
|
|
||||||
/*
|
|
||||||
* We use the cache dir as a temporary directory (since Android doesn't give us one) so
|
|
||||||
* on startup we'll clean up any .tmp files from the last run.
|
|
||||||
*/
|
|
||||||
File[] files = getContext().getCacheDir().listFiles();
|
|
||||||
for (File file : files) {
|
|
||||||
if (file.getName().endsWith(".tmp")) {
|
|
||||||
file.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String getType(Uri uri) {
|
|
||||||
List<String> segments = uri.getPathSegments();
|
|
||||||
String dbName = segments.get(0);
|
|
||||||
String id = segments.get(1);
|
|
||||||
String format = segments.get(2);
|
|
||||||
if (FORMAT_THUMBNAIL.equals(format)) {
|
|
||||||
return "image/png";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
String path = getContext().getDatabasePath(dbName).getAbsolutePath();
|
|
||||||
SQLiteDatabase db = null;
|
|
||||||
Cursor cursor = null;
|
|
||||||
try {
|
|
||||||
db = SQLiteDatabase.openDatabase(path, null, 0);
|
|
||||||
cursor = db.query(
|
|
||||||
"attachments",
|
|
||||||
new String[] { "mime_type" },
|
|
||||||
"id = ?",
|
|
||||||
new String[] { id },
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null);
|
|
||||||
cursor.moveToFirst();
|
|
||||||
String type = cursor.getString(0);
|
|
||||||
cursor.close();
|
|
||||||
db.close();
|
|
||||||
return type;
|
|
||||||
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
if (db != null) {
|
|
||||||
db.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
|
|
||||||
List<String> segments = uri.getPathSegments();
|
|
||||||
String dbName = segments.get(0);
|
|
||||||
String id = segments.get(1);
|
|
||||||
String format = segments.get(2);
|
|
||||||
if (FORMAT_THUMBNAIL.equals(format)) {
|
|
||||||
int width = Integer.parseInt(segments.get(3));
|
|
||||||
int height = Integer.parseInt(segments.get(4));
|
|
||||||
String filename = "thmb_" + dbName + "_" + id;
|
|
||||||
File dir = getContext().getCacheDir();
|
|
||||||
File file = new File(dir, filename);
|
|
||||||
if (!file.exists()) {
|
|
||||||
Uri attachmentUri = getAttachmentUri(dbName, Long.parseLong(id));
|
|
||||||
String type = getType(attachmentUri);
|
|
||||||
try {
|
|
||||||
FileInputStream in = new FileInputStream(
|
|
||||||
new File(getContext().getDatabasePath(dbName + "_att"), id));
|
|
||||||
Bitmap thumbnail = createThumbnail(type, in);
|
|
||||||
thumbnail = thumbnail.createScaledBitmap(thumbnail, width, height, true);
|
|
||||||
FileOutputStream out = new FileOutputStream(file);
|
|
||||||
thumbnail.compress(Bitmap.CompressFormat.PNG, 100, out);
|
|
||||||
out.close();
|
|
||||||
in.close();
|
|
||||||
}
|
|
||||||
catch (IOException ioe) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return ParcelFileDescriptor.open(
|
|
||||||
new File(getContext().getDatabasePath(dbName + "_att"), id),
|
|
||||||
ParcelFileDescriptor.MODE_READ_ONLY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int delete(Uri uri, String arg1, String[] arg2) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Uri insert(Uri uri, ContentValues values) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
|
||||||
String sortOrder) {
|
|
||||||
if (projection == null) {
|
|
||||||
projection =
|
|
||||||
new String[] {
|
|
||||||
AttachmentProviderColumns._ID,
|
|
||||||
AttachmentProviderColumns.DATA,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
List<String> segments = uri.getPathSegments();
|
|
||||||
String dbName = segments.get(0);
|
|
||||||
String id = segments.get(1);
|
|
||||||
String format = segments.get(2);
|
|
||||||
String path = getContext().getDatabasePath(dbName).getAbsolutePath();
|
|
||||||
String name = null;
|
|
||||||
int size = -1;
|
|
||||||
SQLiteDatabase db = null;
|
|
||||||
Cursor cursor = null;
|
|
||||||
try {
|
|
||||||
db = SQLiteDatabase.openDatabase(path, null, 0);
|
|
||||||
cursor = db.query(
|
|
||||||
"attachments",
|
|
||||||
new String[] { "name", "size" },
|
|
||||||
"id = ?",
|
|
||||||
new String[] { id },
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null);
|
|
||||||
if (!cursor.moveToFirst()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
name = cursor.getString(0);
|
|
||||||
size = cursor.getInt(1);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
if (cursor != null) {
|
|
||||||
cursor.close();
|
|
||||||
}
|
|
||||||
if (db != null) {
|
|
||||||
db.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MatrixCursor ret = new MatrixCursor(projection);
|
|
||||||
Object[] values = new Object[projection.length];
|
|
||||||
for (int i = 0, count = projection.length; i < count; i++) {
|
|
||||||
String column = projection[i];
|
|
||||||
if (AttachmentProviderColumns._ID.equals(column)) {
|
|
||||||
values[i] = id;
|
|
||||||
}
|
|
||||||
else if (AttachmentProviderColumns.DATA.equals(column)) {
|
|
||||||
values[i] = uri.toString();
|
|
||||||
}
|
|
||||||
else if (AttachmentProviderColumns.DISPLAY_NAME.equals(column)) {
|
|
||||||
values[i] = name;
|
|
||||||
}
|
|
||||||
else if (AttachmentProviderColumns.SIZE.equals(column)) {
|
|
||||||
values[i] = size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ret.addRow(values);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap createThumbnail(String type, InputStream data) {
|
|
||||||
if(MimeUtility.mimeTypeMatches(type, "image/*")) {
|
|
||||||
return createImageThumbnail(data);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private Bitmap createImageThumbnail(InputStream data) {
|
|
||||||
try {
|
|
||||||
Bitmap bitmap = BitmapFactory.decodeStream(data);
|
|
||||||
return bitmap;
|
|
||||||
}
|
|
||||||
catch (OutOfMemoryError oome) {
|
|
||||||
/*
|
|
||||||
* Improperly downloaded images, corrupt bitmaps and the like can commonly
|
|
||||||
* cause OOME due to invalid allocation sizes. We're happy with a null bitmap in
|
|
||||||
* that case. If the system is really out of memory we'll know about it soon
|
|
||||||
* enough.
|
|
||||||
*/
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,22 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.service;
|
|
||||||
|
|
||||||
import com.fsck.k9.MessagingController;
|
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
|
|
||||||
public class BootReceiver extends BroadcastReceiver {
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) {
|
|
||||||
MailService.actionReschedule(context);
|
|
||||||
}
|
|
||||||
else if (Intent.ACTION_DEVICE_STORAGE_LOW.equals(intent.getAction())) {
|
|
||||||
MailService.actionCancel(context);
|
|
||||||
}
|
|
||||||
else if (Intent.ACTION_DEVICE_STORAGE_OK.equals(intent.getAction())) {
|
|
||||||
MailService.actionReschedule(context);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,193 +0,0 @@
|
|||||||
|
|
||||||
package com.fsck.k9.service;
|
|
||||||
|
|
||||||
import java.util.HashMap;
|
|
||||||
|
|
||||||
import android.app.AlarmManager;
|
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.app.Service;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.os.IBinder;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
import android.util.Config;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.net.Uri;
|
|
||||||
|
|
||||||
import com.fsck.k9.Account;
|
|
||||||
import com.fsck.k9.k9;
|
|
||||||
import com.fsck.k9.MessagingController;
|
|
||||||
import com.fsck.k9.MessagingListener;
|
|
||||||
import com.fsck.k9.Preferences;
|
|
||||||
import com.fsck.k9.R;
|
|
||||||
import com.fsck.k9.activity.Accounts;
|
|
||||||
import com.fsck.k9.activity.FolderMessageList;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*/
|
|
||||||
public class MailService extends Service {
|
|
||||||
private static final String ACTION_CHECK_MAIL = "com.fsck.k9.intent.action.MAIL_SERVICE_WAKEUP";
|
|
||||||
private static final String ACTION_RESCHEDULE = "com.fsck.k9.intent.action.MAIL_SERVICE_RESCHEDULE";
|
|
||||||
private static final String ACTION_CANCEL = "com.fsck.k9.intent.action.MAIL_SERVICE_CANCEL";
|
|
||||||
|
|
||||||
private Listener mListener = new Listener();
|
|
||||||
|
|
||||||
private int mStartId;
|
|
||||||
|
|
||||||
public static void actionReschedule(Context context) {
|
|
||||||
Intent i = new Intent();
|
|
||||||
i.setClass(context, MailService.class);
|
|
||||||
i.setAction(MailService.ACTION_RESCHEDULE);
|
|
||||||
context.startService(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static void actionCancel(Context context) {
|
|
||||||
Intent i = new Intent();
|
|
||||||
i.setClass(context, MailService.class);
|
|
||||||
i.setAction(MailService.ACTION_CANCEL);
|
|
||||||
context.startService(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart(Intent intent, int startId) {
|
|
||||||
super.onStart(intent, startId);
|
|
||||||
this.mStartId = startId;
|
|
||||||
|
|
||||||
MessagingController.getInstance(getApplication()).addListener(mListener);
|
|
||||||
if (ACTION_CHECK_MAIL.equals(intent.getAction())) {
|
|
||||||
if (Config.LOGV) {
|
|
||||||
Log.v(k9.LOG_TAG, "***** MailService *****: checking mail");
|
|
||||||
}
|
|
||||||
MessagingController.getInstance(getApplication()).checkMail(this, null, mListener);
|
|
||||||
}
|
|
||||||
else if (ACTION_CANCEL.equals(intent.getAction())) {
|
|
||||||
if (Config.LOGV) {
|
|
||||||
Log.v(k9.LOG_TAG, "***** MailService *****: cancel");
|
|
||||||
}
|
|
||||||
cancel();
|
|
||||||
stopSelf(startId);
|
|
||||||
}
|
|
||||||
else if (ACTION_RESCHEDULE.equals(intent.getAction())) {
|
|
||||||
if (Config.LOGV) {
|
|
||||||
Log.v(k9.LOG_TAG, "***** MailService *****: reschedule");
|
|
||||||
}
|
|
||||||
reschedule();
|
|
||||||
stopSelf(startId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
MessagingController.getInstance(getApplication()).removeListener(mListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void cancel() {
|
|
||||||
AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
|
|
||||||
Intent i = new Intent();
|
|
||||||
i.setClassName("com.fsck.k9", "com.fsck.k9.service.MailService");
|
|
||||||
i.setAction(ACTION_CHECK_MAIL);
|
|
||||||
PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
|
|
||||||
alarmMgr.cancel(pi);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void reschedule() {
|
|
||||||
AlarmManager alarmMgr = (AlarmManager)getSystemService(Context.ALARM_SERVICE);
|
|
||||||
Intent i = new Intent();
|
|
||||||
i.setClassName("com.fsck.k9", "com.fsck.k9.service.MailService");
|
|
||||||
i.setAction(ACTION_CHECK_MAIL);
|
|
||||||
PendingIntent pi = PendingIntent.getService(this, 0, i, 0);
|
|
||||||
|
|
||||||
int shortestInterval = -1;
|
|
||||||
for (Account account : Preferences.getPreferences(this).getAccounts()) {
|
|
||||||
if (account.getAutomaticCheckIntervalMinutes() != -1
|
|
||||||
&& (account.getAutomaticCheckIntervalMinutes() < shortestInterval || shortestInterval == -1)) {
|
|
||||||
shortestInterval = account.getAutomaticCheckIntervalMinutes();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shortestInterval == -1) {
|
|
||||||
alarmMgr.cancel(pi);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
alarmMgr.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime()
|
|
||||||
+ (shortestInterval * (60 * 1000)), pi);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public IBinder onBind(Intent intent) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Listener extends MessagingListener {
|
|
||||||
HashMap<Account, Integer> accountsWithNewMail = new HashMap<Account, Integer>();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkMailStarted(Context context, Account account) {
|
|
||||||
accountsWithNewMail.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkMailFailed(Context context, Account account, String reason) {
|
|
||||||
reschedule();
|
|
||||||
stopSelf(mStartId);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void synchronizeMailboxFinished(
|
|
||||||
Account account,
|
|
||||||
String folder,
|
|
||||||
int totalMessagesInMailbox,
|
|
||||||
int numNewMessages) {
|
|
||||||
if (account.isNotifyNewMail() && numNewMessages > 0) {
|
|
||||||
accountsWithNewMail.put(account, numNewMessages);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void checkMailFinished(Context context, Account account) {
|
|
||||||
NotificationManager notifMgr = (NotificationManager)context
|
|
||||||
.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
||||||
|
|
||||||
if (accountsWithNewMail.size() > 0) {
|
|
||||||
Notification notif = new Notification(R.drawable.stat_notify_email_generic,
|
|
||||||
getString(R.string.notification_new_title), System.currentTimeMillis());
|
|
||||||
boolean vibrate = false;
|
|
||||||
String ringtone = null;
|
|
||||||
if (accountsWithNewMail.size() > 1) {
|
|
||||||
for (Account account1 : accountsWithNewMail.keySet()) {
|
|
||||||
if (account1.isVibrate()) vibrate = true;
|
|
||||||
if (account1.isNotifyRingtone()) ringtone = account1.getRingtone();
|
|
||||||
}
|
|
||||||
Intent i = new Intent(context, Accounts.class);
|
|
||||||
PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0);
|
|
||||||
notif.setLatestEventInfo(context, getString(R.string.notification_new_title),
|
|
||||||
getString(R.string.notification_new_multi_account_fmt,
|
|
||||||
accountsWithNewMail.size()), pi);
|
|
||||||
} else {
|
|
||||||
Account account1 = accountsWithNewMail.keySet().iterator().next();
|
|
||||||
int totalNewMails = accountsWithNewMail.get(account1);
|
|
||||||
Intent i = FolderMessageList.actionHandleAccountIntent(context, account1, k9.INBOX);
|
|
||||||
PendingIntent pi = PendingIntent.getActivity(context, 0, i, 0);
|
|
||||||
notif.setLatestEventInfo(context, getString(R.string.notification_new_title),
|
|
||||||
getString(R.string.notification_new_one_account_fmt, totalNewMails,
|
|
||||||
account1.getDescription()), pi);
|
|
||||||
vibrate = account1.isVibrate();
|
|
||||||
if (account1.isNotifyRingtone()) ringtone = account1.getRingtone();
|
|
||||||
}
|
|
||||||
notif.defaults = Notification.DEFAULT_LIGHTS;
|
|
||||||
notif.sound = TextUtils.isEmpty(ringtone) ? null : Uri.parse(ringtone);
|
|
||||||
if (vibrate) {
|
|
||||||
notif.defaults |= Notification.DEFAULT_VIBRATE;
|
|
||||||
}
|
|
||||||
notifMgr.notify(1, notif);
|
|
||||||
}
|
|
||||||
|
|
||||||
reschedule();
|
|
||||||
stopSelf(mStartId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user