diff options
Diffstat (limited to 'java/com/android/voicemailomtp')
198 files changed, 34649 insertions, 0 deletions
diff --git a/java/com/android/voicemailomtp/ActivationTask.java b/java/com/android/voicemailomtp/ActivationTask.java new file mode 100644 index 000000000..7de81e685 --- /dev/null +++ b/java/com/android/voicemailomtp/ActivationTask.java @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp; + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.database.ContentObserver; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.provider.Settings; +import android.provider.Settings.Global; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.telecom.PhoneAccountHandle; +import android.telephony.ServiceState; +import android.telephony.TelephonyManager; +import com.android.voicemailomtp.protocol.VisualVoicemailProtocol; +import com.android.voicemailomtp.scheduling.BaseTask; +import com.android.voicemailomtp.scheduling.RetryPolicy; +import com.android.voicemailomtp.sms.StatusMessage; +import com.android.voicemailomtp.sms.StatusSmsFetcher; +import com.android.voicemailomtp.sync.OmtpVvmSourceManager; +import com.android.voicemailomtp.sync.OmtpVvmSyncService; +import com.android.voicemailomtp.sync.SyncTask; +import java.io.IOException; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeoutException; + +/** + * Task to activate the visual voicemail service. A request to activate VVM will be sent to the + * carrier, which will respond with a STATUS SMS. The credentials will be updated from the SMS. If + * the user is not provisioned provisioning will be attempted. Activation happens when the phone + * boots, the SIM is inserted, signal returned when VVM is not activated yet, and when the carrier + * spontaneously sent a STATUS SMS. + */ +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class ActivationTask extends BaseTask { + + private static final String TAG = "VvmActivationTask"; + + private static final int RETRY_TIMES = 4; + private static final int RETRY_INTERVAL_MILLIS = 5_000; + + private static final String EXTRA_MESSAGE_DATA_BUNDLE = "extra_message_data_bundle"; + + @Nullable + private static DeviceProvisionedObserver sDeviceProvisionedObserver; + + private final RetryPolicy mRetryPolicy; + + private Bundle mMessageData; + + public ActivationTask() { + super(TASK_ACTIVATION); + mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS); + addPolicy(mRetryPolicy); + } + + /** + * Has the user gone through the setup wizard yet. + */ + private static boolean isDeviceProvisioned(Context context) { + return Settings.Global.getInt( + context.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) == 1; + } + + /** + * @param messageData The optional bundle from {@link android.provider.VoicemailContract# + * EXTRA_VOICEMAIL_SMS_FIELDS}, if the task is initiated by a status SMS. If null the task will + * request a status SMS itself. + */ + public static void start(Context context, PhoneAccountHandle phoneAccountHandle, + @Nullable Bundle messageData) { + if (!isDeviceProvisioned(context)) { + VvmLog.i(TAG, "Activation requested while device is not provisioned, postponing"); + // Activation might need information such as system language to be set, so wait until + // the setup wizard is finished. The data bundle from the SMS will be re-requested upon + // activation. + queueActivationAfterProvisioned(context, phoneAccountHandle); + return; + } + + Intent intent = BaseTask.createIntent(context, ActivationTask.class, phoneAccountHandle); + if (messageData != null) { + intent.putExtra(EXTRA_MESSAGE_DATA_BUNDLE, messageData); + } + context.startService(intent); + } + + public void onCreate(Context context, Intent intent, int flags, int startId) { + super.onCreate(context, intent, flags, startId); + mMessageData = intent.getParcelableExtra(EXTRA_MESSAGE_DATA_BUNDLE); + } + + @Override + public Intent createRestartIntent() { + Intent intent = super.createRestartIntent(); + // mMessageData is discarded, request a fresh STATUS SMS for retries. + return intent; + } + + @Override + @WorkerThread + public void onExecuteInBackgroundThread() { + Assert.isNotMainThread(); + + PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle(); + if (phoneAccountHandle == null) { + // This should never happen + VvmLog.e(TAG, "null PhoneAccountHandle"); + return; + } + + OmtpVvmCarrierConfigHelper helper = + new OmtpVvmCarrierConfigHelper(getContext(), phoneAccountHandle); + if (!helper.isValid()) { + VvmLog.i(TAG, "VVM not supported on phoneAccountHandle " + phoneAccountHandle); + VoicemailStatus.disable(getContext(), phoneAccountHandle); + return; + } + + // OmtpVvmCarrierConfigHelper can start the activation process; it will pass in a vvm + // content provider URI which we will use. On some occasions, setting that URI will + // fail, so we will perform a few attempts to ensure that the vvm content provider has + // a good chance of being started up. + if (!VoicemailStatus.edit(getContext(), phoneAccountHandle) + .setType(helper.getVvmType()) + .apply()) { + VvmLog.e(TAG, "Failed to configure content provider - " + helper.getVvmType()); + fail(); + } + VvmLog.i(TAG, "VVM content provider configured - " + helper.getVvmType()); + + if (!OmtpVvmSourceManager.getInstance(getContext()) + .isVvmSourceRegistered(phoneAccountHandle)) { + // This account has not been activated before during the lifetime of this boot. + VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(getContext(), + phoneAccountHandle); + if (preferences.getString(OmtpConstants.SERVER_ADDRESS, null) == null) { + // Only show the "activating" message if activation has not been completed before. + // Subsequent activations are more of a status check and usually does not + // concern the user. + helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle), + OmtpEvents.CONFIG_ACTIVATING); + } else { + // The account has been activated on this device before. Pretend it is already + // activated. If there are any activation error it will overwrite this status. + helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle), + OmtpEvents.CONFIG_ACTIVATING_SUBSEQUENT); + } + + } + if (!hasSignal(getContext(), phoneAccountHandle)) { + VvmLog.i(TAG, "Service lost during activation, aborting"); + // Restore the "NO SIGNAL" state since it will be overwritten by the CONFIG_ACTIVATING + // event. + helper.handleEvent(VoicemailStatus.edit(getContext(), phoneAccountHandle), + OmtpEvents.NOTIFICATION_SERVICE_LOST); + // Don't retry, a new activation will be started after the signal returned. + return; + } + + helper.activateSmsFilter(); + VoicemailStatus.Editor status = mRetryPolicy.getVoicemailStatusEditor(); + + VisualVoicemailProtocol protocol = helper.getProtocol(); + + Bundle data; + if (mMessageData != null) { + // The content of STATUS SMS is provided to launch this task, no need to request it + // again. + data = mMessageData; + } else { + try (StatusSmsFetcher fetcher = new StatusSmsFetcher(getContext(), + phoneAccountHandle)) { + protocol.startActivation(helper, fetcher.getSentIntent()); + // Both the fetcher and OmtpMessageReceiver will be triggered, but + // OmtpMessageReceiver will just route the SMS back to ActivationTask, which will be + // rejected because the task is still running. + data = fetcher.get(); + } catch (TimeoutException e) { + // The carrier is expected to return an STATUS SMS within STATUS_SMS_TIMEOUT_MILLIS + // handleEvent() will do the logging. + helper.handleEvent(status, OmtpEvents.CONFIG_STATUS_SMS_TIME_OUT); + fail(); + return; + } catch (CancellationException e) { + VvmLog.e(TAG, "Unable to send status request SMS"); + fail(); + return; + } catch (InterruptedException | ExecutionException | IOException e) { + VvmLog.e(TAG, "can't get future STATUS SMS", e); + fail(); + return; + } + } + + StatusMessage message = new StatusMessage(data); + VvmLog.d(TAG, "STATUS SMS received: st=" + message.getProvisioningStatus() + + ", rc=" + message.getReturnCode()); + + if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_READY)) { + VvmLog.d(TAG, "subscriber ready, no activation required"); + updateSource(getContext(), phoneAccountHandle, status, message); + } else { + if (helper.supportsProvisioning()) { + VvmLog.i(TAG, "Subscriber not ready, start provisioning"); + helper.startProvisioning(this, phoneAccountHandle, status, message, data); + + } else if (message.getProvisioningStatus().equals(OmtpConstants.SUBSCRIBER_NEW)) { + VvmLog.i(TAG, "Subscriber new but provisioning is not supported"); + // Ignore the non-ready state and attempt to use the provided info as is. + // This is probably caused by not completing the new user tutorial. + updateSource(getContext(), phoneAccountHandle, status, message); + } else { + VvmLog.i(TAG, "Subscriber not ready but provisioning is not supported"); + helper.handleEvent(status, OmtpEvents.CONFIG_SERVICE_NOT_AVAILABLE); + } + } + } + + public static void updateSource(Context context, PhoneAccountHandle phone, + VoicemailStatus.Editor status, StatusMessage message) { + OmtpVvmSourceManager vvmSourceManager = + OmtpVvmSourceManager.getInstance(context); + + if (OmtpConstants.SUCCESS.equals(message.getReturnCode())) { + OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context, phone); + helper.handleEvent(status, OmtpEvents.CONFIG_REQUEST_STATUS_SUCCESS); + + // Save the IMAP credentials in preferences so they are persistent and can be retrieved. + VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phone); + message.putStatus(prefs.edit()).apply(); + + // Add the source to indicate that it is active. + vvmSourceManager.addSource(phone); + + SyncTask.start(context, phone, OmtpVvmSyncService.SYNC_FULL_SYNC); + } else { + VvmLog.e(TAG, "Visual voicemail not available for subscriber."); + } + } + + private static boolean hasSignal(Context context, PhoneAccountHandle phoneAccountHandle) { + TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class) + .createForPhoneAccountHandle(phoneAccountHandle); + return telephonyManager.getServiceState().getState() == ServiceState.STATE_IN_SERVICE; + } + + private static void queueActivationAfterProvisioned(Context context, + PhoneAccountHandle phoneAccountHandle) { + if (sDeviceProvisionedObserver == null) { + sDeviceProvisionedObserver = new DeviceProvisionedObserver(context); + context.getContentResolver() + .registerContentObserver(Settings.Global.getUriFor(Global.DEVICE_PROVISIONED), + false, sDeviceProvisionedObserver); + } + sDeviceProvisionedObserver.addPhoneAcountHandle(phoneAccountHandle); + } + + private static class DeviceProvisionedObserver extends ContentObserver { + + private final Context mContext; + private final Set<PhoneAccountHandle> mPhoneAccountHandles = new HashSet<>(); + + private DeviceProvisionedObserver(Context context) { + super(null); + mContext = context; + } + + public void addPhoneAcountHandle(PhoneAccountHandle phoneAccountHandle) { + mPhoneAccountHandles.add(phoneAccountHandle); + } + + @Override + public void onChange(boolean selfChange) { + if (isDeviceProvisioned(mContext)) { + VvmLog.i(TAG, "device provisioned, resuming activation"); + for (PhoneAccountHandle phoneAccountHandle : mPhoneAccountHandles) { + start(mContext, phoneAccountHandle, null); + } + mContext.getContentResolver().unregisterContentObserver(sDeviceProvisionedObserver); + sDeviceProvisionedObserver = null; + } + } + } +} diff --git a/java/com/android/voicemailomtp/AndroidManifest.xml b/java/com/android/voicemailomtp/AndroidManifest.xml new file mode 100644 index 000000000..282a923d2 --- /dev/null +++ b/java/com/android/voicemailomtp/AndroidManifest.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + package="com.android.voicemailomtp" +> + + <uses-sdk + android:minSdkVersion="23" + android:targetSdkVersion="25" /> + + <application + android:allowBackup="false" + android:supportsRtl="true" + android:usesCleartextTraffic="true" + android:defaultToDeviceProtectedStorage="true" + android:directBootAware="true"> + + <activity android:name="com.android.voicemailomtp.settings.VoicemailSettingsActivity" + android:label="@string/voicemail_settings_label"> + <intent-filter > + <!-- DO NOT RENAME. There are existing apps which use this string. --> + <action android:name="com.android.voicemailomtp.CallFeaturesSetting.ADD_VOICEMAIL" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + <intent-filter> + <action android:name="android.telephony.action.CONFIGURE_VOICEMAIL" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + + <receiver android:name="com.android.voicemailomtp.sms.OmtpMessageReceiver" + android:exported="false" + androidprv:systemUserOnly="true"> + <intent-filter> + <action android:name="com.android.vociemailomtp.sms.sms_received"/> + </intent-filter> + </receiver> + + <receiver + android:name="com.android.voicemailomtp.fetch.FetchVoicemailReceiver" + android:exported="true" + android:permission="com.android.voicemail.permission.READ_VOICEMAIL" + androidprv:systemUserOnly="true"> + <intent-filter> + <action android:name="android.intent.action.FETCH_VOICEMAIL" /> + <data + android:scheme="content" + android:host="com.android.voicemail" + android:mimeType="vnd.android.cursor.item/voicemail" /> + </intent-filter> + </receiver> + <receiver + android:name="com.android.voicemailomtp.sync.OmtpVvmSyncReceiver" + android:exported="true" + android:permission="com.android.voicemail.permission.READ_VOICEMAIL" + androidprv:systemUserOnly="true"> + <intent-filter> + <action android:name="android.provider.action.SYNC_VOICEMAIL"/> + </intent-filter> + </receiver> + <receiver + android:name="com.android.voicemailomtp.sync.VoicemailProviderChangeReceiver" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.PROVIDER_CHANGED" /> + <data + android:scheme="content" + android:host="com.android.voicemail" + android:mimeType="vnd.android.cursor.dir/voicemails"/> + </intent-filter> + </receiver> + + <service + android:name="com.android.voicemailomtp.scheduling.TaskSchedulerService" + android:exported="false" /> + + <service + android:name="com.android.voicemailomtp.OmtpService" + android:permission="android.permission.BIND_VISUAL_VOICEMAIL_SERVICE" + android:exported="true"> + <intent-filter> + <action android:name="android.telephony.VisualVoicemailService"/> + </intent-filter> + </service> + <activity android:name=".settings.VoicemailChangePinActivity" + android:exported="false" + android:windowSoftInputMode="stateVisible|adjustResize"> + </activity> + </application> +</manifest> diff --git a/java/com/android/voicemailomtp/Assert.java b/java/com/android/voicemailomtp/Assert.java new file mode 100644 index 000000000..1d295bed1 --- /dev/null +++ b/java/com/android/voicemailomtp/Assert.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp; + +import android.os.Looper; + +/** + * Assertions which will result in program termination. + */ +public class Assert { + + private static Boolean sIsMainThreadForTest; + + public static void isTrue(boolean condition) { + if (!condition) { + throw new AssertionError("Expected condition to be true"); + } + } + + public static void isMainThread() { + if (sIsMainThreadForTest != null) { + isTrue(sIsMainThreadForTest); + return; + } + isTrue(Looper.getMainLooper().equals(Looper.myLooper())); + } + + public static void isNotMainThread() { + if (sIsMainThreadForTest != null) { + isTrue(!sIsMainThreadForTest); + return; + } + isTrue(!Looper.getMainLooper().equals(Looper.myLooper())); + } + + public static void fail() { + throw new AssertionError("Fail"); + } + + /** + * Override the main thread status for tests. Set to null to revert to normal behavior + */ + @NeededForTesting + public static void setIsMainThreadForTesting(Boolean isMainThread) { + sIsMainThreadForTest = isMainThread; + } +} diff --git a/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java b/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java new file mode 100644 index 000000000..6a4b5104a --- /dev/null +++ b/java/com/android/voicemailomtp/DefaultOmtpEventHandler.java @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp; + +import android.content.Context; +import android.provider.VoicemailContract; +import android.provider.VoicemailContract.Status; + +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.OmtpEvents.Type; + +public class DefaultOmtpEventHandler { + + private static final String TAG = "DefErrorCodeHandler"; + + public static void handleEvent(Context context, OmtpVvmCarrierConfigHelper config, + VoicemailStatus.Editor status, OmtpEvents event) { + switch (event.getType()) { + case Type.CONFIGURATION: + handleConfigurationEvent(context, status, event); + break; + case Type.DATA_CHANNEL: + handleDataChannelEvent(context, status, event); + break; + case Type.NOTIFICATION_CHANNEL: + handleNotificationChannelEvent(context, config, status, event); + break; + case Type.OTHER: + handleOtherEvent(context, status, event); + break; + default: + VvmLog.wtf(TAG, "invalid event type " + event.getType() + " for " + event); + } + } + + private static void handleConfigurationEvent(Context context, VoicemailStatus.Editor status, + OmtpEvents event) { + switch (event) { + case CONFIG_DEFAULT_PIN_REPLACED: + case CONFIG_REQUEST_STATUS_SUCCESS: + case CONFIG_PIN_SET: + status + .setConfigurationState(VoicemailContract.Status.CONFIGURATION_STATE_OK) + .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK) + .apply(); + break; + case CONFIG_ACTIVATING: + // Wipe all errors from the last activation. All errors shown should be new errors + // for this activation. + status + .setConfigurationState(Status.CONFIGURATION_STATE_CONFIGURING) + .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK) + .setDataChannelState(Status.DATA_CHANNEL_STATE_OK).apply(); + break; + case CONFIG_ACTIVATING_SUBSEQUENT: + status + .setConfigurationState(Status.CONFIGURATION_STATE_OK) + .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK) + .setDataChannelState(Status.DATA_CHANNEL_STATE_OK).apply(); + break; + case CONFIG_SERVICE_NOT_AVAILABLE: + status + .setConfigurationState(Status.CONFIGURATION_STATE_FAILED) + .apply(); + break; + case CONFIG_STATUS_SMS_TIME_OUT: + status + .setConfigurationState(Status.CONFIGURATION_STATE_FAILED) + .apply(); + break; + default: + VvmLog.wtf(TAG, "invalid configuration event " + event); + } + } + + private static void handleDataChannelEvent(Context context, VoicemailStatus.Editor status, + OmtpEvents event) { + switch (event) { + case DATA_IMAP_OPERATION_STARTED: + case DATA_IMAP_OPERATION_COMPLETED: + status + .setDataChannelState(Status.DATA_CHANNEL_STATE_OK) + .apply(); + break; + + case DATA_NO_CONNECTION: + status + .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION) + .apply(); + break; + + case DATA_NO_CONNECTION_CELLULAR_REQUIRED: + status + .setDataChannelState( + Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED) + .apply(); + break; + case DATA_INVALID_PORT: + status + .setDataChannelState( + VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION) + .apply(); + break; + case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK: + status + .setDataChannelState( + VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_CONNECTION_ERROR) + .apply(); + break; + case DATA_SSL_INVALID_HOST_NAME: + case DATA_CANNOT_ESTABLISH_SSL_SESSION: + case DATA_IOE_ON_OPEN: + case DATA_GENERIC_IMAP_IOE: + status + .setDataChannelState( + VoicemailContract.Status.DATA_CHANNEL_STATE_COMMUNICATION_ERROR) + .apply(); + break; + case DATA_BAD_IMAP_CREDENTIAL: + case DATA_AUTH_UNKNOWN_USER: + case DATA_AUTH_UNKNOWN_DEVICE: + case DATA_AUTH_INVALID_PASSWORD: + case DATA_AUTH_MAILBOX_NOT_INITIALIZED: + case DATA_AUTH_SERVICE_NOT_PROVISIONED: + case DATA_AUTH_SERVICE_NOT_ACTIVATED: + case DATA_AUTH_USER_IS_BLOCKED: + status + .setDataChannelState( + VoicemailContract.Status.DATA_CHANNEL_STATE_BAD_CONFIGURATION) + .apply(); + break; + + case DATA_REJECTED_SERVER_RESPONSE: + case DATA_INVALID_INITIAL_SERVER_RESPONSE: + case DATA_MAILBOX_OPEN_FAILED: + case DATA_SSL_EXCEPTION: + case DATA_ALL_SOCKET_CONNECTION_FAILED: + status + .setDataChannelState( + VoicemailContract.Status.DATA_CHANNEL_STATE_SERVER_ERROR) + .apply(); + break; + + default: + VvmLog.wtf(TAG, "invalid data channel event " + event); + } + } + + private static void handleNotificationChannelEvent(Context context, + OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, OmtpEvents event) { + switch (event) { + case NOTIFICATION_IN_SERVICE: + status + .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_OK) + // Clear the error state. A sync should follow signal return so any error + // will be reposted. + .setDataChannelState(Status.DATA_CHANNEL_STATE_OK) + .apply(); + break; + case NOTIFICATION_SERVICE_LOST: + status.setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION); + if (config.isCellularDataRequired()) { + status.setDataChannelState( + Status.DATA_CHANNEL_STATE_NO_CONNECTION_CELLULAR_REQUIRED); + } + status.apply(); + break; + default: + VvmLog.wtf(TAG, "invalid notification channel event " + event); + } + } + + private static void handleOtherEvent(Context context, VoicemailStatus.Editor status, + OmtpEvents event) { + switch (event) { + case OTHER_SOURCE_REMOVED: + status + .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED) + .setNotificationChannelState( + Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION) + .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION) + .apply(); + break; + default: + VvmLog.wtf(TAG, "invalid other event " + event); + } + } +} diff --git a/java/com/android/voicemailomtp/NeededForTesting.java b/java/com/android/voicemailomtp/NeededForTesting.java new file mode 100644 index 000000000..20517fed8 --- /dev/null +++ b/java/com/android/voicemailomtp/NeededForTesting.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.SOURCE) +public @interface NeededForTesting { + +} diff --git a/java/com/android/voicemailomtp/OmtpConstants.java b/java/com/android/voicemailomtp/OmtpConstants.java new file mode 100644 index 000000000..da2b998b6 --- /dev/null +++ b/java/com/android/voicemailomtp/OmtpConstants.java @@ -0,0 +1,248 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp; + +import android.support.annotation.IntDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.HashMap; +import java.util.Map; + +/** + * Wrapper class to hold relevant OMTP constants as defined in the OMTP spec. <p> In essence this is + * a programmatic representation of the relevant portions of OMTP spec. + */ +public class OmtpConstants { + public static final String SMS_FIELD_SEPARATOR = ";"; + public static final String SMS_KEY_VALUE_SEPARATOR = "="; + public static final String SMS_PREFIX_SEPARATOR = ":"; + + public static final String SYNC_SMS_PREFIX = "SYNC"; + public static final String STATUS_SMS_PREFIX = "STATUS"; + + // This is the format designated by the OMTP spec. + public static final String DATE_TIME_FORMAT = "dd/MM/yyyy HH:mm Z"; + + /** OMTP protocol versions. */ + public static final String PROTOCOL_VERSION1_1 = "11"; + public static final String PROTOCOL_VERSION1_2 = "12"; + public static final String PROTOCOL_VERSION1_3 = "13"; + + ///////////////////////// Client/Mobile originated SMS ////////////////////// + + /** Mobile Originated requests */ + public static final String ACTIVATE_REQUEST = "Activate"; + public static final String DEACTIVATE_REQUEST = "Deactivate"; + public static final String STATUS_REQUEST = "Status"; + + /** fields that can be present in a Mobile Originated OMTP SMS */ + public static final String CLIENT_TYPE = "ct"; + public static final String APPLICATION_PORT = "pt"; + public static final String PROTOCOL_VERSION = "pv"; + + + //////////////////////////////// Sync SMS fields //////////////////////////// + + /** + * Sync SMS fields. + * <p> + * Each string constant is the field's key in the SMS body which is used by the parser to + * identify the field's value, if present, in the SMS body. + */ + + /** + * The event that triggered this SYNC SMS. + * See {@link OmtpConstants#SYNC_TRIGGER_EVENT_VALUES} + */ + public static final String SYNC_TRIGGER_EVENT = "ev"; + public static final String MESSAGE_UID = "id"; + public static final String MESSAGE_LENGTH = "l"; + public static final String NUM_MESSAGE_COUNT = "c"; + /** See {@link OmtpConstants#CONTENT_TYPE_VALUES} */ + public static final String CONTENT_TYPE = "t"; + public static final String SENDER = "s"; + public static final String TIME = "dt"; + + /** + * SYNC message trigger events. + * <p> + * These are the possible values of {@link OmtpConstants#SYNC_TRIGGER_EVENT}. + */ + public static final String NEW_MESSAGE = "NM"; + public static final String MAILBOX_UPDATE = "MBU"; + public static final String GREETINGS_UPDATE = "GU"; + + public static final String[] SYNC_TRIGGER_EVENT_VALUES = { + NEW_MESSAGE, + MAILBOX_UPDATE, + GREETINGS_UPDATE + }; + + /** + * Content types supported by OMTP VVM. + * <p> + * These are the possible values of {@link OmtpConstants#CONTENT_TYPE}. + */ + public static final String VOICE = "v"; + public static final String VIDEO = "o"; + public static final String FAX = "f"; + /** Voice message deposited by an external application */ + public static final String INFOTAINMENT = "i"; + /** Empty Call Capture - i.e. voicemail with no voice message. */ + public static final String ECC = "e"; + + public static final String[] CONTENT_TYPE_VALUES = {VOICE, VIDEO, FAX, INFOTAINMENT, ECC}; + + ////////////////////////////// Status SMS fields //////////////////////////// + + /** + * Status SMS fields. + * <p> + * Each string constant is the field's key in the SMS body which is used by the parser to + * identify the field's value, if present, in the SMS body. + */ + /** See {@link OmtpConstants#PROVISIONING_STATUS_VALUES} */ + public static final String PROVISIONING_STATUS = "st"; + /** See {@link OmtpConstants#RETURN_CODE_VALUES} */ + public static final String RETURN_CODE = "rc"; + /** URL to send users to for activation VVM */ + public static final String SUBSCRIPTION_URL = "rs"; + /** IMAP4/SMTP server IP address or fully qualified domain name */ + public static final String SERVER_ADDRESS = "srv"; + /** Phone number to access voicemails through Telephony User Interface */ + public static final String TUI_ACCESS_NUMBER = "tui"; + public static final String TUI_PASSWORD_LENGTH = "pw_len"; + /** Number to send client origination SMS */ + public static final String CLIENT_SMS_DESTINATION_NUMBER = "dn"; + public static final String IMAP_PORT = "ipt"; + public static final String IMAP_USER_NAME = "u"; + public static final String IMAP_PASSWORD = "pw"; + public static final String SMTP_PORT = "spt"; + public static final String SMTP_USER_NAME = "smtp_u"; + public static final String SMTP_PASSWORD = "smtp_pw"; + + /** + * User provisioning status values. + * <p> + * Referred by {@link OmtpConstants#PROVISIONING_STATUS}. + */ + public static final String SUBSCRIBER_NEW = "N"; + public static final String SUBSCRIBER_READY = "R"; + public static final String SUBSCRIBER_PROVISIONED = "P"; + public static final String SUBSCRIBER_UNKNOWN = "U"; + public static final String SUBSCRIBER_BLOCKED = "B"; + + public static final String[] PROVISIONING_STATUS_VALUES = { + SUBSCRIBER_NEW, + SUBSCRIBER_READY, + SUBSCRIBER_PROVISIONED, + SUBSCRIBER_UNKNOWN, + SUBSCRIBER_BLOCKED + }; + + /** + * The return code included in a status message. + * <p> + * These are the possible values of {@link OmtpConstants#RETURN_CODE}. + */ + public static final String SUCCESS = "0"; + public static final String SYSTEM_ERROR = "1"; + public static final String SUBSCRIBER_ERROR = "2"; + public static final String MAILBOX_UNKNOWN = "3"; + public static final String VVM_NOT_ACTIVATED = "4"; + public static final String VVM_NOT_PROVISIONED = "5"; + public static final String VVM_CLIENT_UKNOWN = "6"; + public static final String VVM_MAILBOX_NOT_INITIALIZED = "7"; + + public static final String[] RETURN_CODE_VALUES = { + SUCCESS, + SYSTEM_ERROR, + SUBSCRIBER_ERROR, + MAILBOX_UNKNOWN, + VVM_NOT_ACTIVATED, + VVM_NOT_PROVISIONED, + VVM_CLIENT_UKNOWN, + VVM_MAILBOX_NOT_INITIALIZED, + }; + + /** + * A map of all the field keys to the possible values they can have. + */ + public static final Map<String, String[]> possibleValuesMap = new HashMap<String, String[]>() {{ + put(SYNC_TRIGGER_EVENT, SYNC_TRIGGER_EVENT_VALUES); + put(CONTENT_TYPE, CONTENT_TYPE_VALUES); + put(PROVISIONING_STATUS, PROVISIONING_STATUS_VALUES); + put(RETURN_CODE, RETURN_CODE_VALUES); + }}; + + /** + * IMAP command extensions + */ + + /** + * OMTP spec v1.3 2.3.1 Change password request syntax + * + * This changes the PIN to access the Telephone User Interface, the traditional voicemail + * system. + */ + public static final String IMAP_CHANGE_TUI_PWD_FORMAT = "XCHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s"; + + /** + * OMTP spec v1.3 2.4.1 Change languate request syntax + * + * This changes the language in the Telephone User Interface. + */ + public static final String IMAP_CHANGE_VM_LANG_FORMAT = "XCHANGE_VM_LANG LANG=%1$s"; + + /** + * OMTP spec v1.3 2.5.1 Close NUT Request syntax + * + * This disables the new user tutorial, the message played to new users calling in the Telephone + * User Interface. + */ + public static final String IMAP_CLOSE_NUT = "XCLOSE_NUT"; + + /** + * Possible NO responses for CHANGE_TUI_PWD + */ + + public static final String RESPONSE_CHANGE_PIN_TOO_SHORT = "password too short"; + public static final String RESPONSE_CHANGE_PIN_TOO_LONG = "password too long"; + public static final String RESPONSE_CHANGE_PIN_TOO_WEAK = "password too weak"; + public static final String RESPONSE_CHANGE_PIN_MISMATCH = "old password mismatch"; + public static final String RESPONSE_CHANGE_PIN_INVALID_CHARACTER = + "password contains invalid characters"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef(value = {CHANGE_PIN_SUCCESS, CHANGE_PIN_TOO_SHORT, CHANGE_PIN_TOO_LONG, + CHANGE_PIN_TOO_WEAK, CHANGE_PIN_MISMATCH, CHANGE_PIN_INVALID_CHARACTER, + CHANGE_PIN_SYSTEM_ERROR}) + + public @interface ChangePinResult { + + } + + public static final int CHANGE_PIN_SUCCESS = 0; + public static final int CHANGE_PIN_TOO_SHORT = 1; + public static final int CHANGE_PIN_TOO_LONG = 2; + public static final int CHANGE_PIN_TOO_WEAK = 3; + public static final int CHANGE_PIN_MISMATCH = 4; + public static final int CHANGE_PIN_INVALID_CHARACTER = 5; + public static final int CHANGE_PIN_SYSTEM_ERROR = 6; + + /** Indicates the client is Google visual voicemail version 1.0. */ + public static final String CLIENT_TYPE_GOOGLE_10 = "google.vvm.10"; +} diff --git a/java/com/android/voicemailomtp/OmtpEvents.java b/java/com/android/voicemailomtp/OmtpEvents.java new file mode 100644 index 000000000..d5c2a8b03 --- /dev/null +++ b/java/com/android/voicemailomtp/OmtpEvents.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp; + +import android.support.annotation.IntDef; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Events internal to the OMTP client. These should be translated into {@link + * android.provider.VoicemailContract.Status} error codes before writing into the voicemail status + * table. + */ +public enum OmtpEvents { + + // Configuration State + CONFIG_REQUEST_STATUS_SUCCESS(Type.CONFIGURATION, true), + + CONFIG_PIN_SET(Type.CONFIGURATION, true), + // The voicemail PIN is replaced with a generated PIN, user should change it. + CONFIG_DEFAULT_PIN_REPLACED(Type.CONFIGURATION, true), + CONFIG_ACTIVATING(Type.CONFIGURATION, true), + // There are already activation records, this is only a book-keeping activation. + CONFIG_ACTIVATING_SUBSEQUENT(Type.CONFIGURATION, true), + CONFIG_STATUS_SMS_TIME_OUT(Type.CONFIGURATION), + CONFIG_SERVICE_NOT_AVAILABLE(Type.CONFIGURATION), + + // Data channel State + + // A new sync has started, old errors in data channel should be cleared. + DATA_IMAP_OPERATION_STARTED(Type.DATA_CHANNEL, true), + // Successfully downloaded/uploaded data from the server, which means the data channel is clear. + DATA_IMAP_OPERATION_COMPLETED(Type.DATA_CHANNEL, true), + // The port provided in the STATUS SMS is invalid. + DATA_INVALID_PORT(Type.DATA_CHANNEL), + // No connection to the internet, and the carrier requires cellular data + DATA_NO_CONNECTION_CELLULAR_REQUIRED(Type.DATA_CHANNEL), + // No connection to the internet. + DATA_NO_CONNECTION(Type.DATA_CHANNEL), + // Address lookup for the server hostname failed. DNS error? + DATA_CANNOT_RESOLVE_HOST_ON_NETWORK(Type.DATA_CHANNEL), + // All destination address that resolves to the server hostname are rejected or timed out + DATA_ALL_SOCKET_CONNECTION_FAILED(Type.DATA_CHANNEL), + // Failed to establish SSL with the server, either with a direct SSL connection or by + // STARTTLS command + DATA_CANNOT_ESTABLISH_SSL_SESSION(Type.DATA_CHANNEL), + // Identity of the server cannot be verified. + DATA_SSL_INVALID_HOST_NAME(Type.DATA_CHANNEL), + // The server rejected our username/password + DATA_BAD_IMAP_CREDENTIAL(Type.DATA_CHANNEL), + + DATA_AUTH_UNKNOWN_USER(Type.DATA_CHANNEL), + DATA_AUTH_UNKNOWN_DEVICE(Type.DATA_CHANNEL), + DATA_AUTH_INVALID_PASSWORD(Type.DATA_CHANNEL), + DATA_AUTH_MAILBOX_NOT_INITIALIZED(Type.DATA_CHANNEL), + DATA_AUTH_SERVICE_NOT_PROVISIONED(Type.DATA_CHANNEL), + DATA_AUTH_SERVICE_NOT_ACTIVATED(Type.DATA_CHANNEL), + DATA_AUTH_USER_IS_BLOCKED(Type.DATA_CHANNEL), + + // A command to the server didn't result with an "OK" or continuation request + DATA_REJECTED_SERVER_RESPONSE(Type.DATA_CHANNEL), + // The server did not greet us with a "OK", possibly not a IMAP server. + DATA_INVALID_INITIAL_SERVER_RESPONSE(Type.DATA_CHANNEL), + // An IOException occurred while trying to open an ImapConnection + // TODO: reduce scope + DATA_IOE_ON_OPEN(Type.DATA_CHANNEL), + // The SELECT command on a mailbox is rejected + DATA_MAILBOX_OPEN_FAILED(Type.DATA_CHANNEL), + // An IOException has occurred + // TODO: reduce scope + DATA_GENERIC_IMAP_IOE(Type.DATA_CHANNEL), + // An SslException has occurred while opening an ImapConnection + // TODO: reduce scope + DATA_SSL_EXCEPTION(Type.DATA_CHANNEL), + + // Notification Channel + + // Cell signal restored, can received VVM SMSs + NOTIFICATION_IN_SERVICE(Type.NOTIFICATION_CHANNEL, true), + // Cell signal lost, cannot received VVM SMSs + NOTIFICATION_SERVICE_LOST(Type.NOTIFICATION_CHANNEL, false), + + + // Other + OTHER_SOURCE_REMOVED(Type.OTHER, false), + + // VVM3 + VVM3_NEW_USER_SETUP_FAILED, + // Table 4. client internal error handling + VVM3_VMG_DNS_FAILURE, + VVM3_SPG_DNS_FAILURE, + VVM3_VMG_CONNECTION_FAILED, + VVM3_SPG_CONNECTION_FAILED, + VVM3_VMG_TIMEOUT, + VVM3_STATUS_SMS_TIMEOUT, + + VVM3_SUBSCRIBER_PROVISIONED, + VVM3_SUBSCRIBER_BLOCKED, + VVM3_SUBSCRIBER_UNKNOWN; + + public static class Type { + + @Retention(RetentionPolicy.SOURCE) + @IntDef({CONFIGURATION, DATA_CHANNEL, NOTIFICATION_CHANNEL, OTHER}) + public @interface Values { + + } + + public static final int CONFIGURATION = 1; + public static final int DATA_CHANNEL = 2; + public static final int NOTIFICATION_CHANNEL = 3; + public static final int OTHER = 4; + } + + private final int mType; + private final boolean mIsSuccess; + + OmtpEvents(int type, boolean isSuccess) { + mType = type; + mIsSuccess = isSuccess; + } + + OmtpEvents(int type) { + mType = type; + mIsSuccess = false; + } + + OmtpEvents() { + mType = Type.OTHER; + mIsSuccess = false; + } + + @Type.Values + public int getType() { + return mType; + } + + public boolean isSuccess() { + return mIsSuccess; + } + +} diff --git a/java/com/android/voicemailomtp/OmtpService.java b/java/com/android/voicemailomtp/OmtpService.java new file mode 100644 index 000000000..261a7cb32 --- /dev/null +++ b/java/com/android/voicemailomtp/OmtpService.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp; + +import android.content.Intent; +import android.telecom.PhoneAccountHandle; +import android.telephony.VisualVoicemailService; +import android.telephony.VisualVoicemailSms; + +import com.android.voicemailomtp.sync.OmtpVvmSourceManager; + +public class OmtpService extends VisualVoicemailService { + + private static String TAG = "VvmOmtpService"; + + public static final String ACTION_SMS_RECEIVED = "com.android.vociemailomtp.sms.sms_received"; + + public static final String EXTRA_VOICEMAIL_SMS = "extra_voicemail_sms"; + + @Override + public void onCellServiceConnected(VisualVoicemailTask task, + final PhoneAccountHandle phoneAccountHandle) { + VvmLog.i(TAG, "onCellServiceConnected"); + ActivationTask + .start(OmtpService.this, phoneAccountHandle, null); + task.finish(); + } + + @Override + public void onSmsReceived(VisualVoicemailTask task, final VisualVoicemailSms sms) { + VvmLog.i(TAG, "onSmsReceived"); + Intent intent = new Intent(ACTION_SMS_RECEIVED); + intent.setPackage(getPackageName()); + intent.putExtra(EXTRA_VOICEMAIL_SMS, sms); + sendBroadcast(intent); + task.finish(); + } + + @Override + public void onSimRemoved(final VisualVoicemailTask task, + final PhoneAccountHandle phoneAccountHandle) { + VvmLog.i(TAG, "onSimRemoved"); + OmtpVvmSourceManager.getInstance(OmtpService.this).removeSource(phoneAccountHandle); + task.finish(); + } + + @Override + public void onStopped(VisualVoicemailTask task) { + VvmLog.i(TAG, "onStopped"); + } +} diff --git a/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java b/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java new file mode 100644 index 000000000..b3e72d215 --- /dev/null +++ b/java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java @@ -0,0 +1,423 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Bundle; +import android.os.PersistableBundle; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.telecom.PhoneAccountHandle; +import android.telephony.CarrierConfigManager; +import android.telephony.TelephonyManager; +import android.telephony.VisualVoicemailService; +import android.telephony.VisualVoicemailSmsFilterSettings; +import android.text.TextUtils; +import android.util.ArraySet; + +import com.android.voicemailomtp.protocol.VisualVoicemailProtocol; +import com.android.voicemailomtp.protocol.VisualVoicemailProtocolFactory; +import com.android.voicemailomtp.sms.StatusMessage; + +import java.util.Arrays; +import java.util.Set; + +/** + * Manages carrier dependent visual voicemail configuration values. The primary source is the value + * retrieved from CarrierConfigManager. If CarrierConfigManager does not provide the config + * (KEY_VVM_TYPE_STRING is empty, or "hidden" configs), then the value hardcoded in telephony will + * be used (in res/xml/vvm_config.xml) + * + * Hidden configs are new configs that are planned for future APIs, or miscellaneous settings that + * may clutter CarrierConfigManager too much. + * + * The current hidden configs are: {@link #getSslPort()} {@link #getDisabledCapabilities()} + */ +public class OmtpVvmCarrierConfigHelper { + + private static final String TAG = "OmtpVvmCarrierCfgHlpr"; + + static final String KEY_VVM_TYPE_STRING = CarrierConfigManager.KEY_VVM_TYPE_STRING; + static final String KEY_VVM_DESTINATION_NUMBER_STRING = + CarrierConfigManager.KEY_VVM_DESTINATION_NUMBER_STRING; + static final String KEY_VVM_PORT_NUMBER_INT = + CarrierConfigManager.KEY_VVM_PORT_NUMBER_INT; + static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING = + CarrierConfigManager.KEY_CARRIER_VVM_PACKAGE_NAME_STRING; + static final String KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY = + "carrier_vvm_package_name_string_array"; + static final String KEY_VVM_PREFETCH_BOOL = + CarrierConfigManager.KEY_VVM_PREFETCH_BOOL; + static final String KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL = + CarrierConfigManager.KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL; + + /** + * @see #getSslPort() + */ + static final String KEY_VVM_SSL_PORT_NUMBER_INT = + "vvm_ssl_port_number_int"; + + /** + * @see #isLegacyModeEnabled() + */ + static final String KEY_VVM_LEGACY_MODE_ENABLED_BOOL = + "vvm_legacy_mode_enabled_bool"; + + /** + * Ban a capability reported by the server from being used. The array of string should be a + * subset of the capabilities returned IMAP CAPABILITY command. + * + * @see #getDisabledCapabilities() + */ + static final String KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY = + "vvm_disabled_capabilities_string_array"; + static final String KEY_VVM_CLIENT_PREFIX_STRING = + "vvm_client_prefix_string"; + + private final Context mContext; + private final PersistableBundle mCarrierConfig; + private final String mVvmType; + private final VisualVoicemailProtocol mProtocol; + private final PersistableBundle mTelephonyConfig; + + private PhoneAccountHandle mPhoneAccountHandle; + + public OmtpVvmCarrierConfigHelper(Context context, PhoneAccountHandle handle) { + mContext = context; + mPhoneAccountHandle = handle; + mCarrierConfig = getCarrierConfig(); + + TelephonyManager telephonyManager = + (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE); + mTelephonyConfig = new TelephonyVvmConfigManager(context.getResources()) + .getConfig(telephonyManager.createForPhoneAccountHandle(mPhoneAccountHandle) + .getSimOperator()); + + mVvmType = getVvmType(); + mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType); + + } + + @VisibleForTesting + OmtpVvmCarrierConfigHelper(Context context, PersistableBundle carrierConfig, + PersistableBundle telephonyConfig) { + mContext = context; + mCarrierConfig = carrierConfig; + mTelephonyConfig = telephonyConfig; + mVvmType = getVvmType(); + mProtocol = VisualVoicemailProtocolFactory.create(mContext.getResources(), mVvmType); + } + + public Context getContext() { + return mContext; + } + + @Nullable + public PhoneAccountHandle getPhoneAccountHandle() { + return mPhoneAccountHandle; + } + + /** + * return whether the carrier's visual voicemail is supported, with KEY_VVM_TYPE_STRING set as a + * known protocol. + */ + public boolean isValid() { + return mProtocol != null; + } + + @Nullable + public String getVvmType() { + return (String) getValue(KEY_VVM_TYPE_STRING); + } + + @Nullable + public VisualVoicemailProtocol getProtocol() { + return mProtocol; + } + + /** + * @returns arbitrary String stored in the config file. Used for protocol specific values. + */ + @Nullable + public String getString(String key) { + return (String) getValue(key); + } + + @Nullable + public Set<String> getCarrierVvmPackageNames() { + Set<String> names = getCarrierVvmPackageNames(mCarrierConfig); + if (names != null) { + return names; + } + return getCarrierVvmPackageNames(mTelephonyConfig); + } + + private static Set<String> getCarrierVvmPackageNames(@Nullable PersistableBundle bundle) { + if (bundle == null) { + return null; + } + Set<String> names = new ArraySet<>(); + if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING)) { + names.add(bundle.getString(KEY_CARRIER_VVM_PACKAGE_NAME_STRING)); + } + if (bundle.containsKey(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY)) { + names.addAll(Arrays.asList( + bundle.getStringArray(KEY_CARRIER_VVM_PACKAGE_NAME_STRING_ARRAY))); + } + if (names.isEmpty()) { + return null; + } + return names; + } + + /** + * For checking upon sim insertion whether visual voicemail should be enabled. This method does + * so by checking if the carrier's voicemail app is installed. + */ + public boolean isEnabledByDefault() { + if (!isValid()) { + return false; + } + + Set<String> carrierPackages = getCarrierVvmPackageNames(); + if (carrierPackages == null) { + return true; + } + for (String packageName : carrierPackages) { + try { + mContext.getPackageManager().getPackageInfo(packageName, 0); + return false; + } catch (NameNotFoundException e) { + // Do nothing. + } + } + return true; + } + + public boolean isCellularDataRequired() { + return (boolean) getValue(KEY_VVM_CELLULAR_DATA_REQUIRED_BOOL, false); + } + + public boolean isPrefetchEnabled() { + return (boolean) getValue(KEY_VVM_PREFETCH_BOOL, true); + } + + + public int getApplicationPort() { + return (int) getValue(KEY_VVM_PORT_NUMBER_INT, 0); + } + + @Nullable + public String getDestinationNumber() { + return (String) getValue(KEY_VVM_DESTINATION_NUMBER_STRING); + } + + /** + * Hidden config. + * + * @return Port to start a SSL IMAP connection directly. + * + * TODO: make config public and add to CarrierConfigManager + */ + public int getSslPort() { + return (int) getValue(KEY_VVM_SSL_PORT_NUMBER_INT, 0); + } + + /** + * Hidden Config. + * + * <p>Sometimes the server states it supports a certain feature but we found they have bug on + * the server side. For example, in b/28717550 the server reported AUTH=DIGEST-MD5 capability + * but using it to login will cause subsequent response to be erroneous. + * + * @return A set of capabilities that is reported by the IMAP CAPABILITY command, but determined + * to have issues and should not be used. + */ + @Nullable + public Set<String> getDisabledCapabilities() { + Set<String> disabledCapabilities = getDisabledCapabilities(mCarrierConfig); + if (disabledCapabilities != null) { + return disabledCapabilities; + } + return getDisabledCapabilities(mTelephonyConfig); + } + + @Nullable + private static Set<String> getDisabledCapabilities(@Nullable PersistableBundle bundle) { + if (bundle == null) { + return null; + } + if (!bundle.containsKey(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY)) { + return null; + } + ArraySet<String> result = new ArraySet<String>(); + result.addAll( + Arrays.asList(bundle.getStringArray(KEY_VVM_DISABLED_CAPABILITIES_STRING_ARRAY))); + return result; + } + + public String getClientPrefix() { + String prefix = (String) getValue(KEY_VVM_CLIENT_PREFIX_STRING); + if (prefix != null) { + return prefix; + } + return "//VVM"; + } + + /** + * Should legacy mode be used when the OMTP VVM client is disabled? + * + * <p>Legacy mode is a mode that on the carrier side visual voicemail is still activated, but on + * the client side all network operations are disabled. SMSs are still monitored so a new + * message SYNC SMS will be translated to show a message waiting indicator, like traditional + * voicemails. + * + * <p>This is for carriers that does not support VVM deactivation so voicemail can continue to + * function without the data cost. + */ + public boolean isLegacyModeEnabled() { + return (boolean) getValue(KEY_VVM_LEGACY_MODE_ENABLED_BOOL, false); + } + + public void startActivation() { + PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle(); + if (phoneAccountHandle == null) { + // This should never happen + // Error logged in getPhoneAccountHandle(). + return; + } + + if (mVvmType == null || mVvmType.isEmpty()) { + // The VVM type is invalid; we should never have gotten here in the first place since + // this is loaded initially in the constructor, and callers should check isValid() + // before trying to start activation anyways. + VvmLog.e(TAG, "startActivation : vvmType is null or empty for account " + + phoneAccountHandle); + return; + } + + if (mProtocol != null) { + ActivationTask.start(mContext, mPhoneAccountHandle, null); + } + } + + public void activateSmsFilter() { + VisualVoicemailService.setSmsFilterSettings(mContext, getPhoneAccountHandle(), + new VisualVoicemailSmsFilterSettings.Builder() + .setClientPrefix(getClientPrefix()) + .build()); + } + + public void startDeactivation() { + if (!isLegacyModeEnabled()) { + // SMS should still be filtered in legacy mode + VisualVoicemailService.setSmsFilterSettings(mContext, getPhoneAccountHandle(), null); + } + if (mProtocol != null) { + mProtocol.startDeactivation(this); + } + } + + public boolean supportsProvisioning() { + if (mProtocol != null) { + return mProtocol.supportsProvisioning(); + } + return false; + } + + public void startProvisioning(ActivationTask task, PhoneAccountHandle phone, + VoicemailStatus.Editor status, StatusMessage message, Bundle data) { + if (mProtocol != null) { + mProtocol.startProvisioning(task, phone, this, status, message, data); + } + } + + public void requestStatus(@Nullable PendingIntent sentIntent) { + if (mProtocol != null) { + mProtocol.requestStatus(this, sentIntent); + } + } + + public void handleEvent(VoicemailStatus.Editor status, OmtpEvents event) { + VvmLog.i(TAG, "OmtpEvent:" + event); + if (mProtocol != null) { + mProtocol.handleEvent(mContext, this, status, event); + } + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("OmtpVvmCarrierConfigHelper ["); + builder.append("phoneAccountHandle: ").append(mPhoneAccountHandle) + .append(", carrierConfig: ").append(mCarrierConfig != null) + .append(", telephonyConfig: ").append(mTelephonyConfig != null) + .append(", type: ").append(getVvmType()) + .append(", destinationNumber: ").append(getDestinationNumber()) + .append(", applicationPort: ").append(getApplicationPort()) + .append(", sslPort: ").append(getSslPort()) + .append(", isEnabledByDefault: ").append(isEnabledByDefault()) + .append(", isCellularDataRequired: ").append(isCellularDataRequired()) + .append(", isPrefetchEnabled: ").append(isPrefetchEnabled()) + .append(", isLegacyModeEnabled: ").append(isLegacyModeEnabled()) + .append("]"); + return builder.toString(); + } + + @Nullable + private PersistableBundle getCarrierConfig() { + + CarrierConfigManager carrierConfigManager = (CarrierConfigManager) + mContext.getSystemService(Context.CARRIER_CONFIG_SERVICE); + if (carrierConfigManager == null) { + VvmLog.w(TAG, "No carrier config service found."); + return null; + } + + PersistableBundle config = TelephonyManagerStub + .getCarrirConfigForPhoneAccountHandle(getContext(), mPhoneAccountHandle); + + if (TextUtils.isEmpty(config.getString(CarrierConfigManager.KEY_VVM_TYPE_STRING))) { + return null; + } + return config; + } + + @Nullable + private Object getValue(String key) { + return getValue(key, null); + } + + @Nullable + private Object getValue(String key, Object defaultValue) { + Object result; + if (mCarrierConfig != null) { + result = mCarrierConfig.get(key); + if (result != null) { + return result; + } + } + if (mTelephonyConfig != null) { + result = mTelephonyConfig.get(key); + if (result != null) { + return result; + } + } + return defaultValue; + } + +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/SubscriptionInfoHelper.java b/java/com/android/voicemailomtp/SubscriptionInfoHelper.java new file mode 100644 index 000000000..b916247ad --- /dev/null +++ b/java/com/android/voicemailomtp/SubscriptionInfoHelper.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2014 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.android.voicemailomtp; + +import android.app.ActionBar; +import android.content.Context; +import android.content.Intent; +import android.content.res.Resources; +import android.text.TextUtils; + +/** + * Helper for manipulating intents or components with subscription-related information. + * + * In settings, subscription ids and labels are passed along to indicate that settings + * are being changed for particular subscriptions. This helper provides functions for + * helping extract this info and perform common operations using this info. + */ +public class SubscriptionInfoHelper { + public static final int NO_SUB_ID = -1; + + // Extra on intent containing the id of a subscription. + public static final String SUB_ID_EXTRA = + "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionId"; + // Extra on intent containing the label of a subscription. + private static final String SUB_LABEL_EXTRA = + "com.android.voicemailomtp.settings.SubscriptionInfoHelper.SubscriptionLabel"; + + private static Context mContext; + + private static int mSubId = NO_SUB_ID; + private static String mSubLabel; + + /** + * Instantiates the helper, by extracting the subscription id and label from the intent. + */ + public SubscriptionInfoHelper(Context context, Intent intent) { + mContext = context; + mSubId = intent.getIntExtra(SUB_ID_EXTRA, NO_SUB_ID); + mSubLabel = intent.getStringExtra(SUB_LABEL_EXTRA); + } + + /** + * Sets the action bar title to the string specified by the given resource id, formatting + * it with the subscription label. This assumes the resource string is formattable with a + * string-type specifier. + * + * If the subscription label does not exists, leave the existing title. + */ + public void setActionBarTitle(ActionBar actionBar, Resources res, int resId) { + if (actionBar == null || TextUtils.isEmpty(mSubLabel)) { + return; + } + + String title = String.format(res.getString(resId), mSubLabel); + actionBar.setTitle(title); + } + + public int getSubId() { + return mSubId; + } +} diff --git a/java/com/android/voicemailomtp/TelephonyManagerStub.java b/java/com/android/voicemailomtp/TelephonyManagerStub.java new file mode 100644 index 000000000..e2e5dacdb --- /dev/null +++ b/java/com/android/voicemailomtp/TelephonyManagerStub.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2017 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.android.voicemailomtp; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build.VERSION_CODES; +import android.os.PersistableBundle; +import android.telecom.PhoneAccount; +import android.telecom.PhoneAccountHandle; +import android.telephony.CarrierConfigManager; +import android.telephony.SubscriptionManager; +import android.telephony.TelephonyManager; +import java.lang.reflect.Method; + +/** + * Temporary stub for public APIs that should be added into telephony manager. + * + * <p>TODO(b/32637799) remove this. + */ +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class TelephonyManagerStub { + + private static final String TAG = "TelephonyManagerStub"; + + public static void showVoicemailNotification(int voicemailCount) { + + } + + /** + * Dismisses the message waiting (voicemail) indicator. + * + * @param subId the subscription id we should dismiss the notification for. + */ + public static void clearMwiIndicator(int subId) { + + } + + public static void setShouldCheckVisualVoicemailConfigurationForMwi(int subId, + boolean enabled) { + + } + + public static int getSubIdForPhoneAccount(Context context, PhoneAccount phoneAccount) { + // Hidden + TelephonyManager telephonyManager = context.getSystemService(TelephonyManager.class); + try { + Method method = TelephonyManager.class + .getMethod("getSubIdForPhoneAccount", PhoneAccount.class); + return (int) method.invoke(telephonyManager, phoneAccount); + } catch (Exception e) { + VvmLog.e(TAG, "reflection call to getSubIdForPhoneAccount failed:", e); + } + return SubscriptionManager.INVALID_SUBSCRIPTION_ID; + } + + public static String getNetworkSpecifierForPhoneAccountHandle(Context context, + PhoneAccountHandle phoneAccountHandle) { + return String.valueOf(SubscriptionManager.getDefaultDataSubscriptionId()); + } + + public static PersistableBundle getCarrirConfigForPhoneAccountHandle(Context context, + PhoneAccountHandle phoneAccountHandle) { + return context.getSystemService(CarrierConfigManager.class).getConfig(); + } +} diff --git a/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java b/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java new file mode 100644 index 000000000..ab13d36ad --- /dev/null +++ b/java/com/android/voicemailomtp/TelephonyVvmConfigManager.java @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp; + +import android.content.res.Resources; +import android.os.PersistableBundle; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.ArrayMap; +import com.android.voicemailomtp.utils.XmlUtils; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Map; +import java.util.Map.Entry; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +/** + * Load and caches telephony vvm config from res/xml/vvm_config.xml + */ +public class TelephonyVvmConfigManager { + + private static final String TAG = "TelephonyVvmCfgMgr"; + + private static final boolean USE_DEBUG_CONFIG = false; + + private static final String TAG_PERSISTABLEMAP = "pbundle_as_map"; + + static final String KEY_MCCMNC = "mccmnc"; + + private static Map<String, PersistableBundle> sCachedConfigs; + + private final Map<String, PersistableBundle> mConfigs; + + public TelephonyVvmConfigManager(Resources resources) { + if (sCachedConfigs == null) { + sCachedConfigs = loadConfigs(resources.getXml(R.xml.vvm_config)); + } + mConfigs = sCachedConfigs; + } + + @VisibleForTesting + TelephonyVvmConfigManager(XmlPullParser parser) { + mConfigs = loadConfigs(parser); + } + + @Nullable + public PersistableBundle getConfig(String mccMnc) { + if (USE_DEBUG_CONFIG) { + return mConfigs.get("TEST"); + } + return mConfigs.get(mccMnc); + } + + private static Map<String, PersistableBundle> loadConfigs(XmlPullParser parser) { + Map<String, PersistableBundle> configs = new ArrayMap<>(); + try { + ArrayList list = readBundleList(parser); + for (Object object : list) { + if (!(object instanceof PersistableBundle)) { + throw new IllegalArgumentException("PersistableBundle expected, got " + object); + } + PersistableBundle bundle = (PersistableBundle) object; + String[] mccMncs = bundle.getStringArray(KEY_MCCMNC); + if (mccMncs == null) { + throw new IllegalArgumentException("MCCMNC is null"); + } + for (String mccMnc : mccMncs) { + configs.put(mccMnc, bundle); + } + } + } catch (IOException | XmlPullParserException e) { + throw new RuntimeException(e); + } + return configs; + } + + @Nullable + public static ArrayList readBundleList(XmlPullParser in) throws IOException, + XmlPullParserException { + final int outerDepth = in.getDepth(); + int event; + while (((event = in.next()) != XmlPullParser.END_DOCUMENT) && + (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) { + if (event == XmlPullParser.START_TAG) { + final String startTag = in.getName(); + final String[] tagName = new String[1]; + in.next(); + return XmlUtils.readThisListXml(in, startTag, tagName, + new MyReadMapCallback(), false); + } + } + return null; + } + + public static PersistableBundle restoreFromXml(XmlPullParser in) throws IOException, + XmlPullParserException { + final int outerDepth = in.getDepth(); + final String startTag = in.getName(); + final String[] tagName = new String[1]; + int event; + while (((event = in.next()) != XmlPullParser.END_DOCUMENT) && + (event != XmlPullParser.END_TAG || in.getDepth() < outerDepth)) { + if (event == XmlPullParser.START_TAG) { + ArrayMap<String, ?> map = + XmlUtils.readThisArrayMapXml(in, startTag, tagName, + new MyReadMapCallback()); + PersistableBundle result = new PersistableBundle(); + for (Entry<String, ?> entry : map.entrySet()) { + Object value = entry.getValue(); + if (value instanceof Integer) { + result.putInt(entry.getKey(), (int) value); + } else if (value instanceof Boolean) { + result.putBoolean(entry.getKey(), (boolean) value); + } else if (value instanceof String) { + result.putString(entry.getKey(), (String) value); + } else if (value instanceof String[]) { + result.putStringArray(entry.getKey(), (String[]) value); + } else if (value instanceof PersistableBundle) { + result.putPersistableBundle(entry.getKey(), (PersistableBundle) value); + } + } + return result; + } + } + return PersistableBundle.EMPTY; + } + + static class MyReadMapCallback implements XmlUtils.ReadMapCallback { + + @Override + public Object readThisUnknownObjectXml(XmlPullParser in, String tag) + throws XmlPullParserException, IOException { + if (TAG_PERSISTABLEMAP.equals(tag)) { + return restoreFromXml(in); + } + throw new XmlPullParserException("Unknown tag=" + tag); + } + } +} diff --git a/java/com/android/voicemailomtp/VisualVoicemailPreferences.java b/java/com/android/voicemailomtp/VisualVoicemailPreferences.java new file mode 100644 index 000000000..5bc2c6951 --- /dev/null +++ b/java/com/android/voicemailomtp/VisualVoicemailPreferences.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import java.util.Set; + +/** + * Save visual voicemail values in shared preferences to be retrieved later. Because a voicemail + * source is tied 1:1 to a phone account, the phone account handle is used in the key for each + * voicemail source and the associated data. + */ +public class VisualVoicemailPreferences { + + private static final String VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX = + "visual_voicemail_"; + + private final SharedPreferences mPreferences; + private final PhoneAccountHandle mPhoneAccountHandle; + + public VisualVoicemailPreferences(Context context, PhoneAccountHandle phoneAccountHandle) { + mPreferences = PreferenceManager.getDefaultSharedPreferences(context); + mPhoneAccountHandle = phoneAccountHandle; + } + + public class Editor { + + private final SharedPreferences.Editor mEditor; + + private Editor() { + mEditor = mPreferences.edit(); + } + + public void apply() { + mEditor.apply(); + } + + public Editor putBoolean(String key, boolean value) { + mEditor.putBoolean(getKey(key), value); + return this; + } + + @NeededForTesting + public Editor putFloat(String key, float value) { + mEditor.putFloat(getKey(key), value); + return this; + } + + public Editor putInt(String key, int value) { + mEditor.putInt(getKey(key), value); + return this; + } + + @NeededForTesting + public Editor putLong(String key, long value) { + mEditor.putLong(getKey(key), value); + return this; + } + + public Editor putString(String key, String value) { + mEditor.putString(getKey(key), value); + return this; + } + + @NeededForTesting + public Editor putStringSet(String key, Set<String> value) { + mEditor.putStringSet(getKey(key), value); + return this; + } + } + + public Editor edit() { + return new Editor(); + } + + public boolean getBoolean(String key, boolean defValue) { + return getValue(key, defValue); + } + + @NeededForTesting + public float getFloat(String key, float defValue) { + return getValue(key, defValue); + } + + public int getInt(String key, int defValue) { + return getValue(key, defValue); + } + + @NeededForTesting + public long getLong(String key, long defValue) { + return getValue(key, defValue); + } + + public String getString(String key, String defValue) { + return getValue(key, defValue); + } + + @Nullable + public String getString(String key) { + return getValue(key, null); + } + + @NeededForTesting + public Set<String> getStringSet(String key, Set<String> defValue) { + return getValue(key, defValue); + } + + public boolean contains(String key) { + return mPreferences.contains(getKey(key)); + } + + private <T> T getValue(String key, T defValue) { + if (!contains(key)) { + return defValue; + } + Object object = mPreferences.getAll().get(getKey(key)); + if (object == null) { + return defValue; + } + return (T) object; + } + + private String getKey(String key) { + return VISUAL_VOICEMAIL_SHARED_PREFS_KEY_PREFIX + key + "_" + mPhoneAccountHandle.getId(); + } +} diff --git a/java/com/android/voicemailomtp/Voicemail.java b/java/com/android/voicemailomtp/Voicemail.java new file mode 100644 index 000000000..9d8395142 --- /dev/null +++ b/java/com/android/voicemailomtp/Voicemail.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp; + +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; + +/** + * Represents a single voicemail stored in the voicemail content provider. + */ +public class Voicemail implements Parcelable { + + private final Long mTimestamp; + private final String mNumber; + private final PhoneAccountHandle mPhoneAccount; + private final Long mId; + private final Long mDuration; + private final String mSource; + private final String mProviderData; + private final Uri mUri; + private final Boolean mIsRead; + private final Boolean mHasContent; + private final String mTranscription; + + private Voicemail(Long timestamp, String number, PhoneAccountHandle phoneAccountHandle, Long id, + Long duration, String source, String providerData, Uri uri, Boolean isRead, + Boolean hasContent, String transcription) { + mTimestamp = timestamp; + mNumber = number; + mPhoneAccount = phoneAccountHandle; + mId = id; + mDuration = duration; + mSource = source; + mProviderData = providerData; + mUri = uri; + mIsRead = isRead; + mHasContent = hasContent; + mTranscription = transcription; + } + + /** + * Create a {@link Builder} for a new {@link Voicemail} to be inserted. <p> The number and the + * timestamp are mandatory for insertion. + */ + public static Builder createForInsertion(long timestamp, String number) { + return new Builder().setNumber(number).setTimestamp(timestamp); + } + + /** + * Create a {@link Builder} for a {@link Voicemail} to be updated (or deleted). <p> The id and + * source data fields are mandatory for update - id is necessary for updating the database and + * source data is necessary for updating the server. + */ + public static Builder createForUpdate(long id, String sourceData) { + return new Builder().setId(id).setSourceData(sourceData); + } + + /** + * Builder pattern for creating a {@link Voicemail}. The builder must be created with the {@link + * #createForInsertion(long, String)} method. <p> This class is <b>not thread safe</b> + */ + public static class Builder { + + private Long mBuilderTimestamp; + private String mBuilderNumber; + private PhoneAccountHandle mBuilderPhoneAccount; + private Long mBuilderId; + private Long mBuilderDuration; + private String mBuilderSourcePackage; + private String mBuilderSourceData; + private Uri mBuilderUri; + private Boolean mBuilderIsRead; + private boolean mBuilderHasContent; + private String mBuilderTranscription; + + /** + * You should use the correct factory method to construct a builder. + */ + private Builder() { + } + + public Builder setNumber(String number) { + mBuilderNumber = number; + return this; + } + + public Builder setTimestamp(long timestamp) { + mBuilderTimestamp = timestamp; + return this; + } + + public Builder setPhoneAccount(PhoneAccountHandle phoneAccount) { + mBuilderPhoneAccount = phoneAccount; + return this; + } + + public Builder setId(long id) { + mBuilderId = id; + return this; + } + + public Builder setDuration(long duration) { + mBuilderDuration = duration; + return this; + } + + public Builder setSourcePackage(String sourcePackage) { + mBuilderSourcePackage = sourcePackage; + return this; + } + + public Builder setSourceData(String sourceData) { + mBuilderSourceData = sourceData; + return this; + } + + public Builder setUri(Uri uri) { + mBuilderUri = uri; + return this; + } + + public Builder setIsRead(boolean isRead) { + mBuilderIsRead = isRead; + return this; + } + + public Builder setHasContent(boolean hasContent) { + mBuilderHasContent = hasContent; + return this; + } + + public Builder setTranscription(String transcription) { + mBuilderTranscription = transcription; + return this; + } + + public Voicemail build() { + mBuilderId = mBuilderId == null ? -1 : mBuilderId; + mBuilderTimestamp = mBuilderTimestamp == null ? 0 : mBuilderTimestamp; + mBuilderDuration = mBuilderDuration == null ? 0 : mBuilderDuration; + mBuilderIsRead = mBuilderIsRead == null ? false : mBuilderIsRead; + return new Voicemail(mBuilderTimestamp, mBuilderNumber, mBuilderPhoneAccount, + mBuilderId, mBuilderDuration, mBuilderSourcePackage, mBuilderSourceData, + mBuilderUri, mBuilderIsRead, mBuilderHasContent, mBuilderTranscription); + } + } + + /** + * The identifier of the voicemail in the content provider. <p> This may be missing in the case + * of a new {@link Voicemail} that we plan to insert into the content provider, since until it + * has been inserted we don't know what id it should have. If none is specified, we return -1. + */ + public long getId() { + return mId; + } + + /** + * The number of the person leaving the voicemail, empty string if unknown, null if not set. + */ + public String getNumber() { + return mNumber; + } + + /** + * The phone account associated with the voicemail, null if not set. + */ + public PhoneAccountHandle getPhoneAccount() { + return mPhoneAccount; + } + + /** + * The timestamp the voicemail was received, in millis since the epoch, zero if not set. + */ + public long getTimestampMillis() { + return mTimestamp; + } + + /** + * Gets the duration of the voicemail in millis, or zero if the field is not set. + */ + public long getDuration() { + return mDuration; + } + + /** + * Returns the package name of the source that added this voicemail, or null if this field is + * not set. + */ + public String getSourcePackage() { + return mSource; + } + + /** + * Returns the application-specific data type stored with the voicemail, or null if this field + * is not set. <p> Source data is typically used as an identifier to uniquely identify the + * voicemail against the voicemail server. This is likely to be something like the IMAP UID, or + * some other server-generated identifying string. + */ + public String getSourceData() { + return mProviderData; + } + + /** + * Gets the Uri that can be used to refer to this voicemail, and to make it play. <p> Returns + * null if we don't know the Uri. + */ + public Uri getUri() { + return mUri; + } + + /** + * Tells us if the voicemail message has been marked as read. <p> Always returns false if this + * field has not been set, i.e. if hasRead() returns false. + */ + public boolean isRead() { + return mIsRead; + } + + /** + * Tells us if there is content stored at the Uri. + */ + public boolean hasContent() { + return mHasContent; + } + + /** + * Returns the text transcription of this voicemail, or null if this field is not set. + */ + public String getTranscription() { + return mTranscription; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(mTimestamp); + writeCharSequence(dest, mNumber); + if (mPhoneAccount == null) { + dest.writeInt(0); + } else { + dest.writeInt(1); + mPhoneAccount.writeToParcel(dest, flags); + } + dest.writeLong(mId); + dest.writeLong(mDuration); + writeCharSequence(dest, mSource); + writeCharSequence(dest, mProviderData); + if (mUri == null) { + dest.writeInt(0); + } else { + dest.writeInt(1); + mUri.writeToParcel(dest, flags); + } + if (mIsRead) { + dest.writeInt(1); + } else { + dest.writeInt(0); + } + if (mHasContent) { + dest.writeInt(1); + } else { + dest.writeInt(0); + } + writeCharSequence(dest, mTranscription); + } + + public static final Creator<Voicemail> CREATOR + = new Creator<Voicemail>() { + @Override + public Voicemail createFromParcel(Parcel in) { + return new Voicemail(in); + } + + @Override + public Voicemail[] newArray(int size) { + return new Voicemail[size]; + } + }; + + private Voicemail(Parcel in) { + mTimestamp = in.readLong(); + mNumber = (String) readCharSequence(in); + if (in.readInt() > 0) { + mPhoneAccount = PhoneAccountHandle.CREATOR.createFromParcel(in); + } else { + mPhoneAccount = null; + } + mId = in.readLong(); + mDuration = in.readLong(); + mSource = (String) readCharSequence(in); + mProviderData = (String) readCharSequence(in); + if (in.readInt() > 0) { + mUri = Uri.CREATOR.createFromParcel(in); + } else { + mUri = null; + } + mIsRead = in.readInt() > 0 ? true : false; + mHasContent = in.readInt() > 0 ? true : false; + mTranscription = (String) readCharSequence(in); + } + + private static CharSequence readCharSequence(Parcel in) { + return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); + } + + public static void writeCharSequence(Parcel dest, CharSequence val) { + TextUtils.writeToParcel(val, dest, 0); + } +} diff --git a/java/com/android/voicemailomtp/VoicemailStatus.java b/java/com/android/voicemailomtp/VoicemailStatus.java new file mode 100644 index 000000000..63007932e --- /dev/null +++ b/java/com/android/voicemailomtp/VoicemailStatus.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.provider.VoicemailContract; +import android.provider.VoicemailContract.Status; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; + +public class VoicemailStatus { + + private static final String TAG = "VvmStatus"; + + public static class Editor { + + private final Context mContext; + @Nullable + private final PhoneAccountHandle mPhoneAccountHandle; + + private ContentValues mValues = new ContentValues(); + + private Editor(Context context, PhoneAccountHandle phoneAccountHandle) { + mContext = context; + mPhoneAccountHandle = phoneAccountHandle; + if (mPhoneAccountHandle == null) { + VvmLog.w(TAG, "VoicemailStatus.Editor created with null phone account, status will" + + " not be written"); + } + } + + @Nullable + public PhoneAccountHandle getPhoneAccountHandle() { + return mPhoneAccountHandle; + } + + public Editor setType(String type) { + mValues.put(Status.SOURCE_TYPE, type); + return this; + } + + public Editor setConfigurationState(int configurationState) { + mValues.put(Status.CONFIGURATION_STATE, configurationState); + return this; + } + + public Editor setDataChannelState(int dataChannelState) { + mValues.put(Status.DATA_CHANNEL_STATE, dataChannelState); + return this; + } + + public Editor setNotificationChannelState(int notificationChannelState) { + mValues.put(Status.NOTIFICATION_CHANNEL_STATE, notificationChannelState); + return this; + } + + public Editor setQuota(int occupied, int total) { + if (occupied == VoicemailContract.Status.QUOTA_UNAVAILABLE + && total == VoicemailContract.Status.QUOTA_UNAVAILABLE) { + return this; + } + + mValues.put(Status.QUOTA_OCCUPIED, occupied); + mValues.put(Status.QUOTA_TOTAL, total); + return this; + } + + /** + * Apply the changes to the {@link VoicemailStatus} {@link #Editor}. + * + * @return {@code true} if the changes were successfully applied, {@code false} otherwise. + */ + public boolean apply() { + if (mPhoneAccountHandle == null) { + return false; + } + mValues.put(Status.PHONE_ACCOUNT_COMPONENT_NAME, + mPhoneAccountHandle.getComponentName().flattenToString()); + mValues.put(Status.PHONE_ACCOUNT_ID, mPhoneAccountHandle.getId()); + ContentResolver contentResolver = mContext.getContentResolver(); + Uri statusUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName()); + try { + contentResolver.insert(statusUri, mValues); + } catch (IllegalArgumentException iae) { + VvmLog.e(TAG, "apply :: failed to insert content resolver ", iae); + mValues.clear(); + return false; + } + mValues.clear(); + return true; + } + + public ContentValues getValues() { + return mValues; + } + } + + /** + * A voicemail status editor that the decision of whether to actually write to the database can + * be deferred. This object will be passed around as a usual {@link Editor}, but {@link + * #apply()} doesn't do anything. If later the creator of this object decides any status changes + * written to it should be committed, {@link #deferredApply()} should be called. + */ + public static class DeferredEditor extends Editor { + + private DeferredEditor(Context context, PhoneAccountHandle phoneAccountHandle) { + super(context, phoneAccountHandle); + } + + @Override + public boolean apply() { + // Do nothing + return true; + } + + public void deferredApply() { + super.apply(); + } + } + + public static Editor edit(Context context, PhoneAccountHandle phoneAccountHandle) { + return new Editor(context, phoneAccountHandle); + } + + /** + * Reset the status to the "disabled" state, which the UI should not show anything for this + * phoneAccountHandle. + */ + public static void disable(Context context, PhoneAccountHandle phoneAccountHandle) { + edit(context, phoneAccountHandle) + .setConfigurationState(Status.CONFIGURATION_STATE_NOT_CONFIGURED) + .setDataChannelState(Status.DATA_CHANNEL_STATE_NO_CONNECTION) + .setNotificationChannelState(Status.NOTIFICATION_CHANNEL_STATE_NO_CONNECTION) + .apply(); + } + + public static DeferredEditor deferredEdit(Context context, + PhoneAccountHandle phoneAccountHandle) { + return new DeferredEditor(context, phoneAccountHandle); + } +} diff --git a/java/com/android/voicemailomtp/VvmLog.java b/java/com/android/voicemailomtp/VvmLog.java new file mode 100644 index 000000000..2add66a53 --- /dev/null +++ b/java/com/android/voicemailomtp/VvmLog.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp; + +import android.util.Log; +import com.android.voicemailomtp.utils.IndentingPrintWriter; +import java.io.FileDescriptor; +import java.io.PrintWriter; +import java.util.ArrayDeque; +import java.util.Calendar; +import java.util.Deque; +import java.util.Iterator; + +/** + * Helper methods for adding to OMTP visual voicemail local logs. + */ +public class VvmLog { + + private static final int MAX_OMTP_VVM_LOGS = 100; + + private static final LocalLog sLocalLog = new LocalLog(MAX_OMTP_VVM_LOGS); + + public static void log(String tag, String log) { + sLocalLog.log(tag + ": " + log); + } + + public static void dump(FileDescriptor fd, PrintWriter printwriter, String[] args) { + IndentingPrintWriter indentingPrintWriter = new IndentingPrintWriter(printwriter, " "); + indentingPrintWriter.increaseIndent(); + sLocalLog.dump(fd, indentingPrintWriter, args); + indentingPrintWriter.decreaseIndent(); + } + + public static int e(String tag, String log) { + log(tag, log); + return Log.e(tag, log); + } + + public static int e(String tag, String log, Throwable e) { + log(tag, log + " " + e); + return Log.e(tag, log, e); + } + + public static int w(String tag, String log) { + log(tag, log); + return Log.w(tag, log); + } + + public static int w(String tag, String log, Throwable e) { + log(tag, log + " " + e); + return Log.w(tag, log, e); + } + + public static int i(String tag, String log) { + log(tag, log); + return Log.i(tag, log); + } + + public static int i(String tag, String log, Throwable e) { + log(tag, log + " " + e); + return Log.i(tag, log, e); + } + + public static int d(String tag, String log) { + log(tag, log); + return Log.d(tag, log); + } + + public static int d(String tag, String log, Throwable e) { + log(tag, log + " " + e); + return Log.d(tag, log, e); + } + + public static int v(String tag, String log) { + log(tag, log); + return Log.v(tag, log); + } + + public static int v(String tag, String log, Throwable e) { + log(tag, log + " " + e); + return Log.v(tag, log, e); + } + + public static int wtf(String tag, String log) { + log(tag, log); + return Log.wtf(tag, log); + } + + public static int wtf(String tag, String log, Throwable e) { + log(tag, log + " " + e); + return Log.wtf(tag, log, e); + } + + /** + * Redact personally identifiable information for production users. If we are running in verbose + * mode, return the original string, otherwise return a SHA-1 hash of the input string. + */ + public static String pii(Object pii) { + if (pii == null) { + return String.valueOf(pii); + } + return "[PII]"; + } + + public static class LocalLog { + + private final Deque<String> mLog; + private final int mMaxLines; + + public LocalLog(int maxLines) { + mMaxLines = Math.max(0, maxLines); + mLog = new ArrayDeque<>(mMaxLines); + } + + public void log(String msg) { + if (mMaxLines <= 0) { + return; + } + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(System.currentTimeMillis()); + append(String.format("%tm-%td %tH:%tM:%tS.%tL - %s", c, c, c, c, c, c, msg)); + } + + private synchronized void append(String logLine) { + while (mLog.size() >= mMaxLines) { + mLog.remove(); + } + mLog.add(logLine); + } + + public synchronized void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + Iterator<String> itr = mLog.iterator(); + while (itr.hasNext()) { + pw.println(itr.next()); + } + } + + public synchronized void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) { + Iterator<String> itr = mLog.descendingIterator(); + while (itr.hasNext()) { + pw.println(itr.next()); + } + } + + public static class ReadOnlyLocalLog { + + private final LocalLog mLog; + + ReadOnlyLocalLog(LocalLog log) { + mLog = log; + } + + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + mLog.dump(fd, pw, args); + } + + public void reverseDump(FileDescriptor fd, PrintWriter pw, String[] args) { + mLog.reverseDump(fd, pw, args); + } + } + + public ReadOnlyLocalLog readOnlyLocalLog() { + return new ReadOnlyLocalLog(this); + } + } +} diff --git a/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java b/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java new file mode 100644 index 000000000..7d9eee9f8 --- /dev/null +++ b/java/com/android/voicemailomtp/VvmPackageInstallReceiver.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.telecom.PhoneAccountHandle; + +import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil; +import com.android.voicemailomtp.sync.OmtpVvmSourceManager; + +import java.util.Set; + +/** + * When a new package is installed, check if it matches any of the vvm carrier apps of the currently + * enabled dialer vvm sources. + */ +public class VvmPackageInstallReceiver extends BroadcastReceiver { + + private static final String TAG = "VvmPkgInstallReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getData() == null) { + return; + } + + String packageName = intent.getData().getSchemeSpecificPart(); + if (packageName == null) { + return; + } + + OmtpVvmSourceManager vvmSourceManager = OmtpVvmSourceManager.getInstance(context); + Set<PhoneAccountHandle> phoneAccounts = vvmSourceManager.getOmtpVvmSources(); + for (PhoneAccountHandle phoneAccount : phoneAccounts) { + if (VisualVoicemailSettingsUtil.isEnabledUserSet(context, phoneAccount)) { + // Skip the check if this voicemail source's setting is overridden by the user. + continue; + } + + OmtpVvmCarrierConfigHelper carrierConfigHelper = new OmtpVvmCarrierConfigHelper( + context, phoneAccount); + if (carrierConfigHelper.getCarrierVvmPackageNames() == null) { + continue; + } + if (carrierConfigHelper.getCarrierVvmPackageNames().contains(packageName)) { + // Force deactivate the client. The user can re-enable it in the settings. + // There are no need to update the settings for deactivation. At this point, if the + // default value is used it should be false because a carrier package is present. + VvmLog.i(TAG, "Carrier VVM package installed, disabling system VVM client"); + OmtpVvmSourceManager.getInstance(context).removeSource(phoneAccount); + carrierConfigHelper.startDeactivation(); + } + } + } +} diff --git a/java/com/android/voicemailomtp/VvmPhoneStateListener.java b/java/com/android/voicemailomtp/VvmPhoneStateListener.java new file mode 100644 index 000000000..1a3013d1f --- /dev/null +++ b/java/com/android/voicemailomtp/VvmPhoneStateListener.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp; + +import android.content.Context; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneStateListener; +import android.telephony.ServiceState; + +import com.android.voicemailomtp.sync.OmtpVvmSourceManager; +import com.android.voicemailomtp.sync.OmtpVvmSyncService; +import com.android.voicemailomtp.sync.SyncTask; +import com.android.voicemailomtp.sync.VoicemailStatusQueryHelper; + +/** + * Check if service is lost and indicate this in the voicemail status. + */ +public class VvmPhoneStateListener extends PhoneStateListener { + + private static final String TAG = "VvmPhoneStateListener"; + + private PhoneAccountHandle mPhoneAccount; + private Context mContext; + private int mPreviousState = -1; + + public VvmPhoneStateListener(Context context, PhoneAccountHandle accountHandle) { + // TODO: b/32637799 too much trouble to call super constructor through reflection, + // just use non-phoneAccountHandle version for now. + super(); + mContext = context; + mPhoneAccount = accountHandle; + } + + @Override + public void onServiceStateChanged(ServiceState serviceState) { + if (mPhoneAccount == null) { + VvmLog.e(TAG, "onServiceStateChanged on phoneAccount " + mPhoneAccount + + " with invalid phoneAccountHandle, ignoring"); + return; + } + + int state = serviceState.getState(); + if (state == mPreviousState || (state != ServiceState.STATE_IN_SERVICE + && mPreviousState != ServiceState.STATE_IN_SERVICE)) { + // Only interested in state changes or transitioning into or out of "in service". + // Otherwise just quit. + mPreviousState = state; + return; + } + + OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, mPhoneAccount); + + if (state == ServiceState.STATE_IN_SERVICE) { + VoicemailStatusQueryHelper voicemailStatusQueryHelper = + new VoicemailStatusQueryHelper(mContext); + if (voicemailStatusQueryHelper.isVoicemailSourceConfigured(mPhoneAccount)) { + if (!voicemailStatusQueryHelper.isNotificationsChannelActive(mPhoneAccount)) { + VvmLog + .v(TAG, "Notifications channel is active for " + mPhoneAccount); + helper.handleEvent(VoicemailStatus.edit(mContext, mPhoneAccount), + OmtpEvents.NOTIFICATION_IN_SERVICE); + } + } + + if (OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(mPhoneAccount)) { + VvmLog + .v(TAG, "Signal returned: requesting resync for " + mPhoneAccount); + // If the source is already registered, run a full sync in case something was missed + // while signal was down. + SyncTask.start(mContext, mPhoneAccount, OmtpVvmSyncService.SYNC_FULL_SYNC); + } else { + VvmLog.v(TAG, + "Signal returned: reattempting activation for " + mPhoneAccount); + // Otherwise initiate an activation because this means that an OMTP source was + // recognized but either the activation text was not successfully sent or a response + // was not received. + helper.startActivation(); + } + } else { + VvmLog.v(TAG, "Notifications channel is inactive for " + mPhoneAccount); + + if (!OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(mPhoneAccount)) { + return; + } + helper.handleEvent(VoicemailStatus.edit(mContext, mPhoneAccount), + OmtpEvents.NOTIFICATION_SERVICE_LOST); + } + mPreviousState = state; + } +} diff --git a/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java b/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java new file mode 100644 index 000000000..85fea80d7 --- /dev/null +++ b/java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.fetch; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Network; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.provider.VoicemailContract; +import android.provider.VoicemailContract.Voicemails; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.os.BuildCompat; +import android.telecom.PhoneAccountHandle; +import android.telephony.TelephonyManager; +import android.text.TextUtils; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.imap.ImapHelper; +import com.android.voicemailomtp.imap.ImapHelper.InitializingException; +import com.android.voicemailomtp.sync.OmtpVvmSourceManager; +import com.android.voicemailomtp.sync.VvmNetworkRequestCallback; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class FetchVoicemailReceiver extends BroadcastReceiver { + + private static final String TAG = "FetchVoicemailReceiver"; + + final static String[] PROJECTION = new String[]{ + Voicemails.SOURCE_DATA, // 0 + Voicemails.PHONE_ACCOUNT_ID, // 1 + Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, // 2 + }; + + public static final int SOURCE_DATA = 0; + public static final int PHONE_ACCOUNT_ID = 1; + public static final int PHONE_ACCOUNT_COMPONENT_NAME = 2; + + // Number of retries + private static final int NETWORK_RETRY_COUNT = 3; + + private ContentResolver mContentResolver; + private Uri mUri; + private VvmNetworkRequestCallback mNetworkCallback; + private Context mContext; + private String mUid; + private PhoneAccountHandle mPhoneAccount; + private int mRetryCount = NETWORK_RETRY_COUNT; + + @Override + public void onReceive(final Context context, Intent intent) { + if (VoicemailContract.ACTION_FETCH_VOICEMAIL.equals(intent.getAction())) { + VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL received"); + mContext = context; + mContentResolver = context.getContentResolver(); + mUri = intent.getData(); + + if (mUri == null) { + VvmLog.w(TAG, + VoicemailContract.ACTION_FETCH_VOICEMAIL + " intent sent with no data"); + return; + } + + if (!context.getPackageName().equals( + mUri.getQueryParameter(VoicemailContract.PARAM_KEY_SOURCE_PACKAGE))) { + // Ignore if the fetch request is for a voicemail not from this package. + VvmLog.e(TAG, + "ACTION_FETCH_VOICEMAIL from foreign pacakge " + context.getPackageName()); + return; + } + + Cursor cursor = mContentResolver.query(mUri, PROJECTION, null, null, null); + if (cursor == null) { + VvmLog.i(TAG, "ACTION_FETCH_VOICEMAIL query returned null"); + return; + } + try { + if (cursor.moveToFirst()) { + mUid = cursor.getString(SOURCE_DATA); + String accountId = cursor.getString(PHONE_ACCOUNT_ID); + if (TextUtils.isEmpty(accountId)) { + TelephonyManager telephonyManager = (TelephonyManager) + context.getSystemService(Context.TELEPHONY_SERVICE); + accountId = telephonyManager.getSimSerialNumber(); + + if (TextUtils.isEmpty(accountId)) { + VvmLog.e(TAG, "Account null and no default sim found."); + return; + } + } + + mPhoneAccount = new PhoneAccountHandle( + ComponentName.unflattenFromString( + cursor.getString(PHONE_ACCOUNT_COMPONENT_NAME)), + cursor.getString(PHONE_ACCOUNT_ID)); + if (!OmtpVvmSourceManager.getInstance(context) + .isVvmSourceRegistered(mPhoneAccount)) { + mPhoneAccount = getAccountFromMarshmallowAccount(context, mPhoneAccount); + if (mPhoneAccount == null) { + VvmLog.w(TAG, "Account not registered - cannot retrieve message."); + return; + } + VvmLog.i(TAG, "Fetching voicemail with Marshmallow PhoneAccountHandle"); + } + VvmLog.i(TAG, "Requesting network to fetch voicemail"); + mNetworkCallback = new fetchVoicemailNetworkRequestCallback(context, + mPhoneAccount); + mNetworkCallback.requestNetwork(); + } + } finally { + cursor.close(); + } + } + } + + /** + * In ag/930496 the format of PhoneAccountHandle has changed between Marshmallow and Nougat. + * This method attempts to search the account from the old database in registered sources using + * the old format. There's a chance of M phone account collisions on multi-SIM devices, but + * visual voicemail is not supported on M multi-SIM. + */ + @Nullable + private static PhoneAccountHandle getAccountFromMarshmallowAccount(Context context, + PhoneAccountHandle oldAccount) { + if (!BuildCompat.isAtLeastN()) { + return null; + } + for (PhoneAccountHandle handle : OmtpVvmSourceManager.getInstance(context) + .getOmtpVvmSources()) { + if (getIccSerialNumberFromFullIccSerialNumber(handle.getId()) + .equals(oldAccount.getId())) { + return handle; + } + } + return null; + } + + /** + * getIccSerialNumber() is used for ID before N, and getFullIccSerialNumber() after. + * getIccSerialNumber() stops at the first hex char. + */ + @NonNull + private static String getIccSerialNumberFromFullIccSerialNumber(@NonNull String id) { + for(int i =0;i<id.length();i++){ + if(!Character.isDigit(id.charAt(i))){ + return id.substring(0,i); + } + } + return id; + } + + private class fetchVoicemailNetworkRequestCallback extends VvmNetworkRequestCallback { + + public fetchVoicemailNetworkRequestCallback(Context context, + PhoneAccountHandle phoneAccount) { + super(context, phoneAccount, VoicemailStatus.edit(context, phoneAccount)); + } + + @Override + public void onAvailable(final Network network) { + super.onAvailable(network); + fetchVoicemail(network, getVoicemailStatusEditor()); + } + } + + private void fetchVoicemail(final Network network, final VoicemailStatus.Editor status) { + Executor executor = Executors.newCachedThreadPool(); + executor.execute(new Runnable() { + @Override + public void run() { + try { + while (mRetryCount > 0) { + VvmLog.i(TAG, "fetching voicemail, retry count=" + mRetryCount); + try (ImapHelper imapHelper = new ImapHelper(mContext, mPhoneAccount, + network, status)) { + boolean success = imapHelper.fetchVoicemailPayload( + new VoicemailFetchedCallback(mContext, mUri, mPhoneAccount), + mUid); + if (!success && mRetryCount > 0) { + VvmLog.i(TAG, "fetch voicemail failed, retrying"); + mRetryCount--; + } else { + return; + } + } catch (InitializingException e) { + VvmLog.w(TAG, "Can't retrieve Imap credentials ", e); + return; + } + } + } finally { + if (mNetworkCallback != null) { + mNetworkCallback.releaseNetwork(); + } + } + } + }); + } +} diff --git a/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java b/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java new file mode 100644 index 000000000..7479c4c4e --- /dev/null +++ b/java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.fetch; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.provider.VoicemailContract.Voicemails; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; +import com.android.voicemailomtp.R; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.imap.VoicemailPayload; +import java.io.IOException; +import java.io.OutputStream; +import org.apache.commons.io.IOUtils; + +/** + * Callback for when a voicemail payload is fetched. It copies the returned stream to the data + * file corresponding to the voicemail. + */ +public class VoicemailFetchedCallback { + private static final String TAG = "VoicemailFetchedCallback"; + + private final Context mContext; + private final ContentResolver mContentResolver; + private final Uri mUri; + private final PhoneAccountHandle mPhoneAccountHandle; + + public VoicemailFetchedCallback(Context context, Uri uri, + PhoneAccountHandle phoneAccountHandle) { + mContext = context; + mContentResolver = context.getContentResolver(); + mUri = uri; + mPhoneAccountHandle = phoneAccountHandle; + } + + /** + * Saves the voicemail payload data into the voicemail provider then sets the "has_content" bit + * of the voicemail to "1". + * + * @param voicemailPayload The object containing the content data for the voicemail + */ + public void setVoicemailContent(@Nullable VoicemailPayload voicemailPayload) { + if (voicemailPayload == null) { + VvmLog.i(TAG, "Payload not found, message has unsupported format"); + ContentValues values = new ContentValues(); + values.put(Voicemails.TRANSCRIPTION, + mContext.getString(R.string.vvm_unsupported_message_format, + mContext.getSystemService(TelecomManager.class) + .getVoiceMailNumber(mPhoneAccountHandle))); + updateVoicemail(values); + return; + } + + VvmLog.d(TAG, String.format("Writing new voicemail content: %s", mUri)); + OutputStream outputStream = null; + + try { + outputStream = mContentResolver.openOutputStream(mUri); + byte[] inputBytes = voicemailPayload.getBytes(); + if (inputBytes != null) { + outputStream.write(inputBytes); + } + } catch (IOException e) { + VvmLog.w(TAG, String.format("File not found for %s", mUri)); + return; + } finally { + IOUtils.closeQuietly(outputStream); + } + + // Update mime_type & has_content after we are done with file update. + ContentValues values = new ContentValues(); + values.put(Voicemails.MIME_TYPE, voicemailPayload.getMimeType()); + values.put(Voicemails.HAS_CONTENT, true); + updateVoicemail(values); + } + + private void updateVoicemail(ContentValues values) { + int updatedCount = mContentResolver.update(mUri, values, null, null); + if (updatedCount != 1) { + VvmLog + .e(TAG, "Updating voicemail should have updated 1 row, was: " + updatedCount); + } + } +} diff --git a/java/com/android/voicemailomtp/imap/ImapHelper.java b/java/com/android/voicemailomtp/imap/ImapHelper.java new file mode 100644 index 000000000..b2a40fb64 --- /dev/null +++ b/java/com/android/voicemailomtp/imap/ImapHelper.java @@ -0,0 +1,711 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.imap; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkInfo; +import android.provider.VoicemailContract; +import android.telecom.PhoneAccountHandle; +import android.util.Base64; + +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.OmtpConstants.ChangePinResult; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VisualVoicemailPreferences; +import com.android.voicemailomtp.Voicemail; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.fetch.VoicemailFetchedCallback; +import com.android.voicemailomtp.mail.Address; +import com.android.voicemailomtp.mail.Body; +import com.android.voicemailomtp.mail.BodyPart; +import com.android.voicemailomtp.mail.FetchProfile; +import com.android.voicemailomtp.mail.Flag; +import com.android.voicemailomtp.mail.Message; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.mail.Multipart; +import com.android.voicemailomtp.mail.TempDirectory; +import com.android.voicemailomtp.mail.internet.MimeMessage; +import com.android.voicemailomtp.mail.store.ImapConnection; +import com.android.voicemailomtp.mail.store.ImapFolder; +import com.android.voicemailomtp.mail.store.ImapStore; +import com.android.voicemailomtp.mail.store.imap.ImapConstants; +import com.android.voicemailomtp.mail.store.imap.ImapResponse; +import com.android.voicemailomtp.mail.utils.LogUtils; +import com.android.voicemailomtp.sync.OmtpVvmSyncService.TranscriptionFetchedCallback; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.Closeable; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import org.apache.commons.io.IOUtils; + +/** + * A helper interface to abstract commands sent across IMAP interface for a given account. + */ +public class ImapHelper implements Closeable { + + private static final String TAG = "ImapHelper"; + + private ImapFolder mFolder; + private ImapStore mImapStore; + + private final Context mContext; + private final PhoneAccountHandle mPhoneAccount; + private final Network mNetwork; + private final VoicemailStatus.Editor mStatus; + + VisualVoicemailPreferences mPrefs; + private static final String PREF_KEY_QUOTA_OCCUPIED = "quota_occupied_"; + private static final String PREF_KEY_QUOTA_TOTAL = "quota_total_"; + + private int mQuotaOccupied; + private int mQuotaTotal; + + private final OmtpVvmCarrierConfigHelper mConfig; + + public class InitializingException extends Exception { + + public InitializingException(String message) { + super(message); + } + } + + public ImapHelper(Context context, PhoneAccountHandle phoneAccount, Network network, + VoicemailStatus.Editor status) + throws InitializingException { + this(context, + new OmtpVvmCarrierConfigHelper( + context, + phoneAccount), + phoneAccount, + network, + status); + } + + public ImapHelper(Context context, OmtpVvmCarrierConfigHelper config, + PhoneAccountHandle phoneAccount, Network network, VoicemailStatus.Editor status) + throws InitializingException { + mContext = context; + mPhoneAccount = phoneAccount; + mNetwork = network; + mStatus = status; + mConfig = config; + mPrefs = new VisualVoicemailPreferences(context, + phoneAccount); + + try { + TempDirectory.setTempDirectory(context); + + String username = mPrefs.getString(OmtpConstants.IMAP_USER_NAME, null); + String password = mPrefs.getString(OmtpConstants.IMAP_PASSWORD, null); + String serverName = mPrefs.getString(OmtpConstants.SERVER_ADDRESS, null); + int port = Integer.parseInt( + mPrefs.getString(OmtpConstants.IMAP_PORT, null)); + int auth = ImapStore.FLAG_NONE; + + int sslPort = mConfig.getSslPort(); + if (sslPort != 0) { + port = sslPort; + auth = ImapStore.FLAG_SSL; + } + + mImapStore = new ImapStore( + context, this, username, password, port, serverName, auth, network); + } catch (NumberFormatException e) { + handleEvent(OmtpEvents.DATA_INVALID_PORT); + LogUtils.w(TAG, "Could not parse port number"); + throw new InitializingException("cannot initialize ImapHelper:" + e.toString()); + } + + mQuotaOccupied = mPrefs + .getInt(PREF_KEY_QUOTA_OCCUPIED, VoicemailContract.Status.QUOTA_UNAVAILABLE); + mQuotaTotal = mPrefs + .getInt(PREF_KEY_QUOTA_TOTAL, VoicemailContract.Status.QUOTA_UNAVAILABLE); + } + + @Override + public void close() { + mImapStore.closeConnection(); + } + + public boolean isRoaming() { + ConnectivityManager connectivityManager = (ConnectivityManager) mContext.getSystemService( + Context.CONNECTIVITY_SERVICE); + NetworkInfo info = connectivityManager.getNetworkInfo(mNetwork); + if (info == null) { + return false; + } + return info.isRoaming(); + } + + public OmtpVvmCarrierConfigHelper getConfig() { + return mConfig; + } + + public ImapConnection connect() { + return mImapStore.getConnection(); + } + + /** + * The caller thread will block until the method returns. + */ + public boolean markMessagesAsRead(List<Voicemail> voicemails) { + return setFlags(voicemails, Flag.SEEN); + } + + /** + * The caller thread will block until the method returns. + */ + public boolean markMessagesAsDeleted(List<Voicemail> voicemails) { + return setFlags(voicemails, Flag.DELETED); + } + + public void handleEvent(OmtpEvents event) { + mConfig.handleEvent(mStatus, event); + } + + /** + * Set flags on the server for a given set of voicemails. + * + * @param voicemails The voicemails to set flags for. + * @param flags The flags to set on the voicemails. + * @return {@code true} if the operation completes successfully, {@code false} otherwise. + */ + private boolean setFlags(List<Voicemail> voicemails, String... flags) { + if (voicemails.size() == 0) { + return false; + } + try { + mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE); + if (mFolder != null) { + mFolder.setFlags(convertToImapMessages(voicemails), flags, true); + return true; + } + return false; + } catch (MessagingException e) { + LogUtils.e(TAG, e, "Messaging exception"); + return false; + } finally { + closeImapFolder(); + } + } + + /** + * Fetch a list of voicemails from the server. + * + * @return A list of voicemail objects containing data about voicemails stored on the server. + */ + public List<Voicemail> fetchAllVoicemails() { + List<Voicemail> result = new ArrayList<Voicemail>(); + Message[] messages; + try { + mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE); + if (mFolder == null) { + // This means we were unable to successfully open the folder. + return null; + } + + // This method retrieves lightweight messages containing only the uid of the message. + messages = mFolder.getMessages(null); + + for (Message message : messages) { + // Get the voicemail details (message structure). + MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message); + if (messageStructureWrapper != null) { + result.add(getVoicemailFromMessageStructure(messageStructureWrapper)); + } + } + return result; + } catch (MessagingException e) { + LogUtils.e(TAG, e, "Messaging Exception"); + return null; + } finally { + closeImapFolder(); + } + } + + /** + * Extract voicemail details from the message structure. Also fetch transcription if a + * transcription exists. + */ + private Voicemail getVoicemailFromMessageStructure( + MessageStructureWrapper messageStructureWrapper) throws MessagingException { + Message messageDetails = messageStructureWrapper.messageStructure; + + TranscriptionFetchedListener listener = new TranscriptionFetchedListener(); + if (messageStructureWrapper.transcriptionBodyPart != null) { + FetchProfile fetchProfile = new FetchProfile(); + fetchProfile.add(messageStructureWrapper.transcriptionBodyPart); + + mFolder.fetch(new Message[]{messageDetails}, fetchProfile, listener); + } + + // Found an audio attachment, this is a valid voicemail. + long time = messageDetails.getSentDate().getTime(); + String number = getNumber(messageDetails.getFrom()); + boolean isRead = Arrays.asList(messageDetails.getFlags()).contains(Flag.SEEN); + return Voicemail.createForInsertion(time, number) + .setPhoneAccount(mPhoneAccount) + .setSourcePackage(mContext.getPackageName()) + .setSourceData(messageDetails.getUid()) + .setIsRead(isRead) + .setTranscription(listener.getVoicemailTranscription()) + .build(); + } + + /** + * The "from" field of a visual voicemail IMAP message is the number of the caller who left the + * message. Extract this number from the list of "from" addresses. + * + * @param fromAddresses A list of addresses that comprise the "from" line. + * @return The number of the voicemail sender. + */ + private String getNumber(Address[] fromAddresses) { + if (fromAddresses != null && fromAddresses.length > 0) { + if (fromAddresses.length != 1) { + LogUtils.w(TAG, "More than one from addresses found. Using the first one."); + } + String sender = fromAddresses[0].getAddress(); + int atPos = sender.indexOf('@'); + if (atPos != -1) { + // Strip domain part of the address. + sender = sender.substring(0, atPos); + } + return sender; + } + return null; + } + + /** + * Fetches the structure of the given message and returns a wrapper containing the message + * structure and the transcription structure (if applicable). + * + * @throws MessagingException if fetching the structure of the message fails + */ + private MessageStructureWrapper fetchMessageStructure(Message message) + throws MessagingException { + LogUtils.d(TAG, "Fetching message structure for " + message.getUid()); + + MessageStructureFetchedListener listener = new MessageStructureFetchedListener(); + + FetchProfile fetchProfile = new FetchProfile(); + fetchProfile.addAll(Arrays.asList(FetchProfile.Item.FLAGS, FetchProfile.Item.ENVELOPE, + FetchProfile.Item.STRUCTURE)); + + // The IMAP folder fetch method will call "messageRetrieved" on the listener when the + // message is successfully retrieved. + mFolder.fetch(new Message[]{message}, fetchProfile, listener); + return listener.getMessageStructure(); + } + + public boolean fetchVoicemailPayload(VoicemailFetchedCallback callback, final String uid) { + try { + mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE); + if (mFolder == null) { + // This means we were unable to successfully open the folder. + return false; + } + Message message = mFolder.getMessage(uid); + if (message == null) { + return false; + } + VoicemailPayload voicemailPayload = fetchVoicemailPayload(message); + callback.setVoicemailContent(voicemailPayload); + return true; + } catch (MessagingException e) { + } finally { + closeImapFolder(); + } + return false; + } + + /** + * Fetches the body of the given message and returns the parsed voicemail payload. + * + * @throws MessagingException if fetching the body of the message fails + */ + private VoicemailPayload fetchVoicemailPayload(Message message) + throws MessagingException { + LogUtils.d(TAG, "Fetching message body for " + message.getUid()); + + MessageBodyFetchedListener listener = new MessageBodyFetchedListener(); + + FetchProfile fetchProfile = new FetchProfile(); + fetchProfile.add(FetchProfile.Item.BODY); + + mFolder.fetch(new Message[]{message}, fetchProfile, listener); + return listener.getVoicemailPayload(); + } + + public boolean fetchTranscription(TranscriptionFetchedCallback callback, String uid) { + try { + mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE); + if (mFolder == null) { + // This means we were unable to successfully open the folder. + return false; + } + + Message message = mFolder.getMessage(uid); + if (message == null) { + return false; + } + + MessageStructureWrapper messageStructureWrapper = fetchMessageStructure(message); + if (messageStructureWrapper != null) { + TranscriptionFetchedListener listener = new TranscriptionFetchedListener(); + if (messageStructureWrapper.transcriptionBodyPart != null) { + FetchProfile fetchProfile = new FetchProfile(); + fetchProfile.add(messageStructureWrapper.transcriptionBodyPart); + + // This method is called synchronously so the transcription will be populated + // in the listener once the next method is called. + mFolder.fetch(new Message[]{message}, fetchProfile, listener); + callback.setVoicemailTranscription(listener.getVoicemailTranscription()); + } + } + return true; + } catch (MessagingException e) { + LogUtils.e(TAG, e, "Messaging Exception"); + return false; + } finally { + closeImapFolder(); + } + } + + + @ChangePinResult + public int changePin(String oldPin, String newPin) + throws MessagingException { + ImapConnection connection = mImapStore.getConnection(); + try { + String command = getConfig().getProtocol() + .getCommand(OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT); + connection.sendCommand( + String.format(Locale.US, command, newPin, oldPin), true); + return getChangePinResultFromImapResponse(connection.readResponse()); + } catch (IOException ioe) { + VvmLog.e(TAG, "changePin: ", ioe); + return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR; + } finally { + connection.destroyResponses(); + } + } + + public void changeVoicemailTuiLanguage(String languageCode) + throws MessagingException { + ImapConnection connection = mImapStore.getConnection(); + try { + String command = getConfig().getProtocol() + .getCommand(OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT); + connection.sendCommand( + String.format(Locale.US, command, languageCode), true); + } catch (IOException ioe) { + LogUtils.e(TAG, ioe.toString()); + } finally { + connection.destroyResponses(); + } + } + + public void closeNewUserTutorial() throws MessagingException { + ImapConnection connection = mImapStore.getConnection(); + try { + String command = getConfig().getProtocol() + .getCommand(OmtpConstants.IMAP_CLOSE_NUT); + connection.executeSimpleCommand(command, false); + } catch (IOException ioe) { + throw new MessagingException(MessagingException.SERVER_ERROR, ioe.toString()); + } finally { + connection.destroyResponses(); + } + } + + @ChangePinResult + private static int getChangePinResultFromImapResponse(ImapResponse response) + throws MessagingException { + if (!response.isTagged()) { + throw new MessagingException(MessagingException.SERVER_ERROR, + "tagged response expected"); + } + if (!response.isOk()) { + String message = response.getStringOrEmpty(1).getString(); + LogUtils.d(TAG, "change PIN failed: " + message); + if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_SHORT.equals(message)) { + return OmtpConstants.CHANGE_PIN_TOO_SHORT; + } + if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_LONG.equals(message)) { + return OmtpConstants.CHANGE_PIN_TOO_LONG; + } + if (OmtpConstants.RESPONSE_CHANGE_PIN_TOO_WEAK.equals(message)) { + return OmtpConstants.CHANGE_PIN_TOO_WEAK; + } + if (OmtpConstants.RESPONSE_CHANGE_PIN_MISMATCH.equals(message)) { + return OmtpConstants.CHANGE_PIN_MISMATCH; + } + if (OmtpConstants.RESPONSE_CHANGE_PIN_INVALID_CHARACTER.equals(message)) { + return OmtpConstants.CHANGE_PIN_INVALID_CHARACTER; + } + return OmtpConstants.CHANGE_PIN_SYSTEM_ERROR; + } + LogUtils.d(TAG, "change PIN succeeded"); + return OmtpConstants.CHANGE_PIN_SUCCESS; + } + + public void updateQuota() { + try { + mFolder = openImapFolder(ImapFolder.MODE_READ_WRITE); + if (mFolder == null) { + // This means we were unable to successfully open the folder. + return; + } + updateQuota(mFolder); + } catch (MessagingException e) { + LogUtils.e(TAG, e, "Messaging Exception"); + } finally { + closeImapFolder(); + } + } + + private void updateQuota(ImapFolder folder) throws MessagingException { + setQuota(folder.getQuota()); + } + + private void setQuota(ImapFolder.Quota quota) { + if (quota == null) { + return; + } + if (quota.occupied == mQuotaOccupied && quota.total == mQuotaTotal) { + VvmLog.v(TAG, "Quota hasn't changed"); + return; + } + mQuotaOccupied = quota.occupied; + mQuotaTotal = quota.total; + VoicemailStatus.edit(mContext, mPhoneAccount) + .setQuota(mQuotaOccupied, mQuotaTotal) + .apply(); + mPrefs.edit() + .putInt(PREF_KEY_QUOTA_OCCUPIED, mQuotaOccupied) + .putInt(PREF_KEY_QUOTA_TOTAL, mQuotaTotal) + .apply(); + VvmLog.v(TAG, "Quota changed to " + mQuotaOccupied + "/" + mQuotaTotal); + } + + /** + * A wrapper to hold a message with its header details and the structure for transcriptions (so + * they can be fetched in the future). + */ + public class MessageStructureWrapper { + + public Message messageStructure; + public BodyPart transcriptionBodyPart; + + public MessageStructureWrapper() { + } + } + + /** + * Listener for the message structure being fetched. + */ + private final class MessageStructureFetchedListener + implements ImapFolder.MessageRetrievalListener { + + private MessageStructureWrapper mMessageStructure; + + public MessageStructureFetchedListener() { + } + + public MessageStructureWrapper getMessageStructure() { + return mMessageStructure; + } + + @Override + public void messageRetrieved(Message message) { + LogUtils.d(TAG, "Fetched message structure for " + message.getUid()); + LogUtils.d(TAG, "Message retrieved: " + message); + try { + mMessageStructure = getMessageOrNull(message); + if (mMessageStructure == null) { + LogUtils.d(TAG, "This voicemail does not have an attachment..."); + return; + } + } catch (MessagingException e) { + LogUtils.e(TAG, e, "Messaging Exception"); + closeImapFolder(); + } + } + + /** + * Check if this IMAP message is a valid voicemail and whether it contains a transcription. + * + * @param message The IMAP message. + * @return The MessageStructureWrapper object corresponding to an IMAP message and + * transcription. + */ + private MessageStructureWrapper getMessageOrNull(Message message) + throws MessagingException { + if (!message.getMimeType().startsWith("multipart/")) { + LogUtils.w(TAG, "Ignored non multi-part message"); + return null; + } + + MessageStructureWrapper messageStructureWrapper = new MessageStructureWrapper(); + + Multipart multipart = (Multipart) message.getBody(); + for (int i = 0; i < multipart.getCount(); ++i) { + BodyPart bodyPart = multipart.getBodyPart(i); + String bodyPartMimeType = bodyPart.getMimeType().toLowerCase(); + LogUtils.d(TAG, "bodyPart mime type: " + bodyPartMimeType); + + if (bodyPartMimeType.startsWith("audio/")) { + messageStructureWrapper.messageStructure = message; + } else if (bodyPartMimeType.startsWith("text/")) { + messageStructureWrapper.transcriptionBodyPart = bodyPart; + } else { + VvmLog.v(TAG, "Unknown bodyPart MIME: " + bodyPartMimeType); + } + } + + if (messageStructureWrapper.messageStructure != null) { + return messageStructureWrapper; + } + + // No attachment found, this is not a voicemail. + return null; + } + } + + /** + * Listener for the message body being fetched. + */ + private final class MessageBodyFetchedListener implements ImapFolder.MessageRetrievalListener { + + private VoicemailPayload mVoicemailPayload; + + /** + * Returns the fetch voicemail payload. + */ + public VoicemailPayload getVoicemailPayload() { + return mVoicemailPayload; + } + + @Override + public void messageRetrieved(Message message) { + LogUtils.d(TAG, "Fetched message body for " + message.getUid()); + LogUtils.d(TAG, "Message retrieved: " + message); + try { + mVoicemailPayload = getVoicemailPayloadFromMessage(message); + } catch (MessagingException e) { + LogUtils.e(TAG, "Messaging Exception:", e); + } catch (IOException e) { + LogUtils.e(TAG, "IO Exception:", e); + } + } + + private VoicemailPayload getVoicemailPayloadFromMessage(Message message) + throws MessagingException, IOException { + Multipart multipart = (Multipart) message.getBody(); + List<String> mimeTypes = new ArrayList<>(); + for (int i = 0; i < multipart.getCount(); ++i) { + BodyPart bodyPart = multipart.getBodyPart(i); + String bodyPartMimeType = bodyPart.getMimeType().toLowerCase(); + mimeTypes.add(bodyPartMimeType); + if (bodyPartMimeType.startsWith("audio/")) { + byte[] bytes = getDataFromBody(bodyPart.getBody()); + LogUtils.d(TAG, String.format("Fetched %s bytes of data", bytes.length)); + return new VoicemailPayload(bodyPartMimeType, bytes); + } + } + LogUtils.e(TAG, "No audio attachment found on this voicemail, mimeTypes:" + mimeTypes); + return null; + } + } + + /** + * Listener for the transcription being fetched. + */ + private final class TranscriptionFetchedListener implements + ImapFolder.MessageRetrievalListener { + + private String mVoicemailTranscription; + + /** + * Returns the fetched voicemail transcription. + */ + public String getVoicemailTranscription() { + return mVoicemailTranscription; + } + + @Override + public void messageRetrieved(Message message) { + LogUtils.d(TAG, "Fetched transcription for " + message.getUid()); + try { + mVoicemailTranscription = new String(getDataFromBody(message.getBody())); + } catch (MessagingException e) { + LogUtils.e(TAG, "Messaging Exception:", e); + } catch (IOException e) { + LogUtils.e(TAG, "IO Exception:", e); + } + } + } + + private ImapFolder openImapFolder(String modeReadWrite) { + try { + if (mImapStore == null) { + return null; + } + ImapFolder folder = new ImapFolder(mImapStore, ImapConstants.INBOX); + folder.open(modeReadWrite); + return folder; + } catch (MessagingException e) { + LogUtils.e(TAG, e, "Messaging Exception"); + } + return null; + } + + private Message[] convertToImapMessages(List<Voicemail> voicemails) { + Message[] messages = new Message[voicemails.size()]; + for (int i = 0; i < voicemails.size(); ++i) { + messages[i] = new MimeMessage(); + messages[i].setUid(voicemails.get(i).getSourceData()); + } + return messages; + } + + private void closeImapFolder() { + if (mFolder != null) { + mFolder.close(true); + } + } + + private byte[] getDataFromBody(Body body) throws IOException, MessagingException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + BufferedOutputStream bufferedOut = new BufferedOutputStream(out); + try { + body.writeTo(bufferedOut); + return Base64.decode(out.toByteArray(), Base64.DEFAULT); + } finally { + IOUtils.closeQuietly(bufferedOut); + IOUtils.closeQuietly(out); + } + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/imap/VoicemailPayload.java b/java/com/android/voicemailomtp/imap/VoicemailPayload.java new file mode 100644 index 000000000..04c69dea5 --- /dev/null +++ b/java/com/android/voicemailomtp/imap/VoicemailPayload.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.imap; + +/** + * The payload for a voicemail, usually audio data. + */ +public class VoicemailPayload { + private final String mMimeType; + private final byte[] mBytes; + + public VoicemailPayload(String mimeType, byte[] bytes) { + mMimeType = mimeType; + mBytes = bytes; + } + + public byte[] getBytes() { + return mBytes; + } + + public String getMimeType() { + return mMimeType; + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/Address.java b/java/com/android/voicemailomtp/mail/Address.java new file mode 100644 index 000000000..ed3f44c03 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/Address.java @@ -0,0 +1,541 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.VisibleForTesting; +import android.text.Html; +import android.text.TextUtils; +import android.text.util.Rfc822Token; +import android.text.util.Rfc822Tokenizer; +import com.android.voicemailomtp.mail.utils.LogUtils; +import java.util.ArrayList; +import java.util.regex.Pattern; +import org.apache.james.mime4j.codec.EncoderUtil; +import org.apache.james.mime4j.decoder.DecoderUtil; + +/** + * This class represent email address. + * + * RFC822 email address may have following format. + * "name" <address> (comment) + * "name" <address> + * name <address> + * address + * Name and comment part should be MIME/base64 encoded in header if necessary. + * + */ +public class Address implements Parcelable { + public static final String ADDRESS_DELIMETER = ","; + /** + * Address part, in the form local_part@domain_part. No surrounding angle brackets. + */ + private String mAddress; + + /** + * Name part. No surrounding double quote, and no MIME/base64 encoding. + * This must be null if Address has no name part. + */ + private String mPersonal; + + /** + * When personal is set, it will return the first token of the personal + * string. Otherwise, it will return the e-mail address up to the '@' sign. + */ + private String mSimplifiedName; + + // Regex that matches address surrounded by '<>' optionally. '^<?([^>]+)>?$' + private static final Pattern REMOVE_OPTIONAL_BRACKET = Pattern.compile("^<?([^>]+)>?$"); + // Regex that matches personal name surrounded by '""' optionally. '^"?([^"]+)"?$' + private static final Pattern REMOVE_OPTIONAL_DQUOTE = Pattern.compile("^\"?([^\"]*)\"?$"); + // Regex that matches escaped character '\\([\\"])' + private static final Pattern UNQUOTE = Pattern.compile("\\\\([\\\\\"])"); + + // TODO: LOCAL_PART and DOMAIN_PART_PART are too permissive and can be improved. + // TODO: Fix this to better constrain comments. + /** Regex for the local part of an email address. */ + private static final String LOCAL_PART = "[^@]+"; + /** Regex for each part of the domain part, i.e. the thing between the dots. */ + private static final String DOMAIN_PART_PART = "[[\\w][\\d]\\-\\(\\)\\[\\]]+"; + /** Regex for the domain part, which is two or more {@link #DOMAIN_PART_PART} separated by . */ + private static final String DOMAIN_PART = + "(" + DOMAIN_PART_PART + "\\.)+" + DOMAIN_PART_PART; + + /** Pattern to check if an email address is valid. */ + private static final Pattern EMAIL_ADDRESS = + Pattern.compile("\\A" + LOCAL_PART + "@" + DOMAIN_PART + "\\z"); + + private static final Address[] EMPTY_ADDRESS_ARRAY = new Address[0]; + + // delimiters are chars that do not appear in an email address, used by fromHeader + private static final char LIST_DELIMITER_EMAIL = '\1'; + private static final char LIST_DELIMITER_PERSONAL = '\2'; + + private static final String LOG_TAG = "Email Address"; + + @VisibleForTesting + public Address(String address) { + setAddress(address); + } + + public Address(String address, String personal) { + setPersonal(personal); + setAddress(address); + } + + /** + * Returns a simplified string for this e-mail address. + * When a name is known, it will return the first token of that name. Otherwise, it will + * return the e-mail address up to the '@' sign. + */ + public String getSimplifiedName() { + if (mSimplifiedName == null) { + if (TextUtils.isEmpty(mPersonal) && !TextUtils.isEmpty(mAddress)) { + int atSign = mAddress.indexOf('@'); + mSimplifiedName = (atSign != -1) ? mAddress.substring(0, atSign) : ""; + } else if (!TextUtils.isEmpty(mPersonal)) { + + // TODO: use Contacts' NameSplitter for more reliable first-name extraction + + int end = mPersonal.indexOf(' '); + while (end > 0 && mPersonal.charAt(end - 1) == ',') { + end--; + } + mSimplifiedName = (end < 1) ? mPersonal : mPersonal.substring(0, end); + + } else { + LogUtils.w(LOG_TAG, "Unable to get a simplified name"); + mSimplifiedName = ""; + } + } + return mSimplifiedName; + } + + public static synchronized Address getEmailAddress(String rawAddress) { + if (TextUtils.isEmpty(rawAddress)) { + return null; + } + String name, address; + final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(rawAddress); + if (tokens.length > 0) { + final String tokenizedName = tokens[0].getName(); + name = tokenizedName != null ? Html.fromHtml(tokenizedName.trim()).toString() + : ""; + address = Html.fromHtml(tokens[0].getAddress()).toString(); + } else { + name = ""; + address = rawAddress == null ? + "" : Html.fromHtml(rawAddress).toString(); + } + return new Address(address, name); + } + + public String getAddress() { + return mAddress; + } + + public void setAddress(String address) { + mAddress = REMOVE_OPTIONAL_BRACKET.matcher(address).replaceAll("$1"); + } + + /** + * Get name part as UTF-16 string. No surrounding double quote, and no MIME/base64 encoding. + * + * @return Name part of email address. Returns null if it is omitted. + */ + public String getPersonal() { + return mPersonal; + } + + /** + * Set personal part from UTF-16 string. Optional surrounding double quote will be removed. + * It will be also unquoted and MIME/base64 decoded. + * + * @param personal name part of email address as UTF-16 string. Null is acceptable. + */ + public void setPersonal(String personal) { + mPersonal = decodeAddressPersonal(personal); + } + + /** + * Decodes name from UTF-16 string. Optional surrounding double quote will be removed. + * It will be also unquoted and MIME/base64 decoded. + * + * @param personal name part of email address as UTF-16 string. Null is acceptable. + */ + public static String decodeAddressPersonal(String personal) { + if (personal != null) { + personal = REMOVE_OPTIONAL_DQUOTE.matcher(personal).replaceAll("$1"); + personal = UNQUOTE.matcher(personal).replaceAll("$1"); + personal = DecoderUtil.decodeEncodedWords(personal); + if (personal.length() == 0) { + personal = null; + } + } + return personal; + } + + /** + * This method is used to check that all the addresses that the user + * entered in a list (e.g. To:) are valid, so that none is dropped. + */ + @VisibleForTesting + public static boolean isAllValid(String addressList) { + // This code mimics the parse() method below. + // I don't know how to better avoid the code-duplication. + if (addressList != null && addressList.length() > 0) { + Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList); + for (int i = 0, length = tokens.length; i < length; ++i) { + Rfc822Token token = tokens[i]; + String address = token.getAddress(); + if (!TextUtils.isEmpty(address) && !isValidAddress(address)) { + return false; + } + } + } + return true; + } + + /** + * Parse a comma-delimited list of addresses in RFC822 format and return an + * array of Address objects. + * + * @param addressList Address list in comma-delimited string. + * @return An array of 0 or more Addresses. + */ + public static Address[] parse(String addressList) { + if (addressList == null || addressList.length() == 0) { + return EMPTY_ADDRESS_ARRAY; + } + Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(addressList); + ArrayList<Address> addresses = new ArrayList<Address>(); + for (int i = 0, length = tokens.length; i < length; ++i) { + Rfc822Token token = tokens[i]; + String address = token.getAddress(); + if (!TextUtils.isEmpty(address)) { + if (isValidAddress(address)) { + String name = token.getName(); + if (TextUtils.isEmpty(name)) { + name = null; + } + addresses.add(new Address(address, name)); + } + } + } + return addresses.toArray(new Address[addresses.size()]); + } + + /** + * Checks whether a string email address is valid. + * E.g. name@domain.com is valid. + */ + @VisibleForTesting + static boolean isValidAddress(final String address) { + return EMAIL_ADDRESS.matcher(address).find(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof Address) { + // It seems that the spec says that the "user" part is case-sensitive, + // while the domain part in case-insesitive. + // So foo@yahoo.com and Foo@yahoo.com are different. + // This may seem non-intuitive from the user POV, so we + // may re-consider it if it creates UI trouble. + // A problem case is "replyAll" sending to both + // a@b.c and to A@b.c, which turn out to be the same on the server. + // Leave unchanged for now (i.e. case-sensitive). + return getAddress().equals(((Address) o).getAddress()); + } + return super.equals(o); + } + + @Override + public int hashCode() { + return getAddress().hashCode(); + } + + /** + * Get human readable address string. + * Do not use this for email header. + * + * @return Human readable address string. Not quoted and not encoded. + */ + @Override + public String toString() { + if (mPersonal != null && !mPersonal.equals(mAddress)) { + if (mPersonal.matches(".*[\\(\\)<>@,;:\\\\\".\\[\\]].*")) { + return ensureQuotedString(mPersonal) + " <" + mAddress + ">"; + } else { + return mPersonal + " <" + mAddress + ">"; + } + } else { + return mAddress; + } + } + + /** + * 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) -> "" + * " -> "" + */ + private static String ensureQuotedString(String s) { + if (s == null) { + return null; + } + if (!s.matches("^\".*\"$")) { + return "\"" + s + "\""; + } else { + return s; + } + } + + /** + * Get human readable comma-delimited address string. + * + * @param addresses Address array + * @return Human readable comma-delimited address string. + */ + @VisibleForTesting + public static String toString(Address[] addresses) { + return toString(addresses, ADDRESS_DELIMETER); + } + + /** + * Get human readable address strings joined with the specified separator. + * + * @param addresses Address array + * @param separator Separator + * @return Human readable comma-delimited address string. + */ + public static String toString(Address[] addresses, String separator) { + if (addresses == null || addresses.length == 0) { + return null; + } + if (addresses.length == 1) { + return addresses[0].toString(); + } + StringBuilder sb = new StringBuilder(addresses[0].toString()); + for (int i = 1; i < addresses.length; i++) { + sb.append(separator); + // TODO: investigate why this .trim() is needed. + sb.append(addresses[i].toString().trim()); + } + return sb.toString(); + } + + /** + * Get RFC822/MIME compatible address string. + * + * @return RFC822/MIME compatible address string. + * It may be surrounded by double quote or quoted and MIME/base64 encoded if necessary. + */ + public String toHeader() { + if (mPersonal != null) { + return EncoderUtil.encodeAddressDisplayName(mPersonal) + " <" + mAddress + ">"; + } else { + return mAddress; + } + } + + /** + * Get RFC822/MIME compatible comma-delimited address string. + * + * @param addresses Address array + * @return RFC822/MIME compatible comma-delimited address string. + * it may be surrounded by double quoted or quoted and MIME/base64 encoded if necessary. + */ + public static String toHeader(Address[] addresses) { + if (addresses == null || addresses.length == 0) { + return null; + } + if (addresses.length == 1) { + return addresses[0].toHeader(); + } + StringBuilder sb = new StringBuilder(addresses[0].toHeader()); + for (int i = 1; i < addresses.length; i++) { + // We need space character to be able to fold line. + sb.append(", "); + sb.append(addresses[i].toHeader()); + } + return sb.toString(); + } + + /** + * Get Human friendly address string. + * + * @return the personal part of this Address, or the address part if the + * personal part is not available + */ + @VisibleForTesting + public String toFriendly() { + if (mPersonal != null && mPersonal.length() > 0) { + return mPersonal; + } else { + return mAddress; + } + } + + /** + * Creates a comma-delimited list of addresses in the "friendly" format (see toFriendly() for + * details on the per-address conversion). + * + * @param addresses Array of Address[] values + * @return A comma-delimited string listing all of the addresses supplied. Null if source + * was null or empty. + */ + @VisibleForTesting + public static String toFriendly(Address[] addresses) { + if (addresses == null || addresses.length == 0) { + return null; + } + if (addresses.length == 1) { + return addresses[0].toFriendly(); + } + StringBuilder sb = new StringBuilder(addresses[0].toFriendly()); + for (int i = 1; i < addresses.length; i++) { + sb.append(", "); + sb.append(addresses[i].toFriendly()); + } + return sb.toString(); + } + + /** + * Returns exactly the same result as Address.toString(Address.fromHeader(addressList)). + */ + @VisibleForTesting + public static String fromHeaderToString(String addressList) { + return toString(fromHeader(addressList)); + } + + /** + * Returns exactly the same result as Address.toHeader(Address.parse(addressList)). + */ + @VisibleForTesting + public static String parseToHeader(String addressList) { + return Address.toHeader(Address.parse(addressList)); + } + + /** + * Returns null if the addressList has 0 addresses, otherwise returns the first address. + * The same as Address.fromHeader(addressList)[0] for non-empty list. + * This is an utility method that offers some performance optimization opportunities. + */ + @VisibleForTesting + public static Address firstAddress(String addressList) { + Address[] array = fromHeader(addressList); + return array.length > 0 ? array[0] : null; + } + + /** + * This method exists to convert an address list formatted in a deprecated legacy format to the + * standard RFC822 header format. {@link #fromHeader(String)} is capable of reading the legacy + * format and the RFC822 format. {@link #toHeader()} always produces the RFC822 format. + * + * This implementation is brute-force, and could be replaced with a more efficient version + * if desired. + */ + public static String reformatToHeader(String addressList) { + return toHeader(fromHeader(addressList)); + } + + /** + * @param addressList a CSV of RFC822 addresses or the deprecated legacy string format + * @return array of addresses parsed from <code>addressList</code> + */ + @VisibleForTesting + public static Address[] fromHeader(String addressList) { + if (addressList == null || addressList.length() == 0) { + return EMPTY_ADDRESS_ARRAY; + } + // IF we're CSV, just parse + if ((addressList.indexOf(LIST_DELIMITER_PERSONAL) == -1) && + (addressList.indexOf(LIST_DELIMITER_EMAIL) == -1)) { + return Address.parse(addressList); + } + // Otherwise, do backward-compatible unpack + ArrayList<Address> addresses = new ArrayList<Address>(); + int length = addressList.length(); + int pairStartIndex = 0; + int pairEndIndex; + + /* addressEndIndex is only re-scanned (indexOf()) when a LIST_DELIMITER_PERSONAL + is used, not for every email address; i.e. not for every iteration of the while(). + This reduces the theoretical complexity from quadratic to linear, + and provides some speed-up in practice by removing redundant scans of the string. + */ + int addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL); + + while (pairStartIndex < length) { + pairEndIndex = addressList.indexOf(LIST_DELIMITER_EMAIL, pairStartIndex); + if (pairEndIndex == -1) { + pairEndIndex = length; + } + Address address; + if (addressEndIndex == -1 || pairEndIndex <= addressEndIndex) { + // in this case the DELIMITER_PERSONAL is in a future pair, + // so don't use personal, and don't update addressEndIndex + address = new Address(addressList.substring(pairStartIndex, pairEndIndex), null); + } else { + address = new Address(addressList.substring(pairStartIndex, addressEndIndex), + addressList.substring(addressEndIndex + 1, pairEndIndex)); + // only update addressEndIndex when we use the LIST_DELIMITER_PERSONAL + addressEndIndex = addressList.indexOf(LIST_DELIMITER_PERSONAL, pairEndIndex + 1); + } + addresses.add(address); + pairStartIndex = pairEndIndex + 1; + } + return addresses.toArray(new Address[addresses.size()]); + } + + public static final Creator<Address> CREATOR = new Creator<Address>() { + @Override + public Address createFromParcel(Parcel parcel) { + return new Address(parcel); + } + + @Override + public Address[] newArray(int size) { + return new Address[size]; + } + }; + + public Address(Parcel in) { + setPersonal(in.readString()); + setAddress(in.readString()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel out, int flags) { + out.writeString(mPersonal); + out.writeString(mAddress); + } +} diff --git a/java/com/android/voicemailomtp/mail/AuthenticationFailedException.java b/java/com/android/voicemailomtp/mail/AuthenticationFailedException.java new file mode 100644 index 000000000..995d5d348 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/AuthenticationFailedException.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +public class AuthenticationFailedException extends MessagingException { + public static final long serialVersionUID = -1; + + public AuthenticationFailedException(String message) { + super(MessagingException.AUTHENTICATION_FAILED, message); + } + + public AuthenticationFailedException(int exceptionType, String message) { + super(exceptionType, message); + } + + public AuthenticationFailedException(String message, Throwable throwable) { + super(MessagingException.AUTHENTICATION_FAILED, message, throwable); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/Base64Body.java b/java/com/android/voicemailomtp/mail/Base64Body.java new file mode 100644 index 000000000..6e1deff44 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/Base64Body.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +import android.util.Base64; +import android.util.Base64OutputStream; + +import org.apache.commons.io.IOUtils; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class Base64Body implements Body { + private final InputStream mSource; + // Because we consume the input stream, we can only write out once + private boolean mAlreadyWritten; + + public Base64Body(InputStream source) { + mSource = source; + } + + @Override + public InputStream getInputStream() throws MessagingException { + return mSource; + } + + /** + * This method consumes the input stream, so can only be called once + * @param out Stream to write to + * @throws IllegalStateException If called more than once + * @throws IOException + * @throws MessagingException + */ + @Override + public void writeTo(OutputStream out) + throws IllegalStateException, IOException, MessagingException { + if (mAlreadyWritten) { + throw new IllegalStateException("Base64Body can only be written once"); + } + mAlreadyWritten = true; + try { + final Base64OutputStream b64out = new Base64OutputStream(out, Base64.DEFAULT); + IOUtils.copyLarge(mSource, b64out); + } finally { + mSource.close(); + } + } +} diff --git a/java/com/android/voicemailomtp/mail/Body.java b/java/com/android/voicemailomtp/mail/Body.java new file mode 100644 index 000000000..393e1823c --- /dev/null +++ b/java/com/android/voicemailomtp/mail/Body.java @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.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; +} diff --git a/java/com/android/voicemailomtp/mail/BodyPart.java b/java/com/android/voicemailomtp/mail/BodyPart.java new file mode 100644 index 000000000..62390a43e --- /dev/null +++ b/java/com/android/voicemailomtp/mail/BodyPart.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +public abstract class BodyPart implements Part { + protected Multipart mParent; + + public Multipart getParent() { + return mParent; + } +} diff --git a/java/com/android/voicemailomtp/mail/CertificateValidationException.java b/java/com/android/voicemailomtp/mail/CertificateValidationException.java new file mode 100644 index 000000000..8ebe5480b --- /dev/null +++ b/java/com/android/voicemailomtp/mail/CertificateValidationException.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +public class CertificateValidationException extends MessagingException { + public static final long serialVersionUID = -1; + + public CertificateValidationException(String message) { + super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message); + } + + public CertificateValidationException(String message, Throwable throwable) { + super(MessagingException.CERTIFICATE_VALIDATION_ERROR, message, throwable); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/FetchProfile.java b/java/com/android/voicemailomtp/mail/FetchProfile.java new file mode 100644 index 000000000..d050692cc --- /dev/null +++ b/java/com/android/voicemailomtp/mail/FetchProfile.java @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.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<Fetchable> { + /** + * 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 implements Fetchable { + /** + * 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 generally be around 50kB. + */ + BODY_SANE, + + /** + * The entire message. + */ + BODY, + } + + /** + * @return the first {@link Part} in this collection, or null if it doesn't contain + * {@link Part}. + */ + public Part getFirstPart() { + for (Fetchable o : this) { + if (o instanceof Part) { + return (Part) o; + } + } + return null; + } +} diff --git a/java/com/android/voicemailomtp/mail/Fetchable.java b/java/com/android/voicemailomtp/mail/Fetchable.java new file mode 100644 index 000000000..1d8d0005b --- /dev/null +++ b/java/com/android/voicemailomtp/mail/Fetchable.java @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +/** + * Interface for classes that can be added to {@link FetchProfile}. + * i.e. {@link Part} and its subclasses, and {@link FetchProfile.Item}. + */ +public interface Fetchable { +} diff --git a/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java b/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java new file mode 100644 index 000000000..65655efd5 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/FixedLengthInputStream.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +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 final InputStream mIn; + private final 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 int getLength() { + return mLength; + } + + @Override + public String toString() { + return String.format("FixedLengthInputStream(in=%s, length=%d)", mIn.toString(), mLength); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/Flag.java b/java/com/android/voicemailomtp/mail/Flag.java new file mode 100644 index 000000000..a9f927099 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/Flag.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +/** + * Flags that can be applied to Messages. + */ +public class Flag { + // If adding new flags: ALL FLAGS MUST BE UPPER CASE. + public static final String DELETED = "deleted"; + public static final String SEEN = "seen"; + public static final String ANSWERED = "answered"; + public static final String FLAGGED = "flagged"; + public static final String DRAFT = "draft"; + public static final String RECENT = "recent"; +} diff --git a/java/com/android/voicemailomtp/mail/MailTransport.java b/java/com/android/voicemailomtp/mail/MailTransport.java new file mode 100644 index 000000000..3bf851fd8 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/MailTransport.java @@ -0,0 +1,344 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +import android.content.Context; +import android.net.Network; +import android.support.annotation.VisibleForTesting; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.imap.ImapHelper; +import com.android.voicemailomtp.mail.store.ImapStore; +import com.android.voicemailomtp.mail.utils.LogUtils; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.ArrayList; +import java.util.List; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; + +/** + * Make connection and perform operations on mail server by reading and writing lines. + */ +public class MailTransport { + private static final String TAG = "MailTransport"; + + // TODO protected eventually + /*protected*/ public static final int SOCKET_CONNECT_TIMEOUT = 10000; + /*protected*/ public static final int SOCKET_READ_TIMEOUT = 60000; + + private static final HostnameVerifier HOSTNAME_VERIFIER = + HttpsURLConnection.getDefaultHostnameVerifier(); + + private final Context mContext; + private final ImapHelper mImapHelper; + private final Network mNetwork; + private final String mHost; + private final int mPort; + private Socket mSocket; + private BufferedInputStream mIn; + private BufferedOutputStream mOut; + private final int mFlags; + private SocketCreator mSocketCreator; + private InetSocketAddress mAddress; + + public MailTransport(Context context, ImapHelper imapHelper, Network network, String address, + int port, int flags) { + mContext = context; + mImapHelper = imapHelper; + mNetwork = network; + mHost = address; + mPort = port; + mFlags = flags; + } + + /** + * Returns a new transport, using the current transport as a model. The new transport is + * configured identically, but not opened or connected in any way. + */ + @Override + public MailTransport clone() { + return new MailTransport(mContext, mImapHelper, mNetwork, mHost, mPort, mFlags); + } + + public boolean canTrySslSecurity() { + return (mFlags & ImapStore.FLAG_SSL) != 0; + } + + public boolean canTrustAllCertificates() { + return (mFlags & ImapStore.FLAG_TRUST_ALL) != 0; + } + + /** + * Attempts to open a connection using the Uri supplied for connection parameters. Will attempt + * an SSL connection if indicated. + */ + public void open() throws MessagingException { + LogUtils.d(TAG, "*** IMAP open " + mHost + ":" + String.valueOf(mPort)); + + List<InetSocketAddress> socketAddresses = new ArrayList<InetSocketAddress>(); + + if (mNetwork == null) { + socketAddresses.add(new InetSocketAddress(mHost, mPort)); + } else { + try { + InetAddress[] inetAddresses = mNetwork.getAllByName(mHost); + if (inetAddresses.length == 0) { + throw new MessagingException(MessagingException.IOERROR, + "Host name " + mHost + "cannot be resolved on designated network"); + } + for (int i = 0; i < inetAddresses.length; i++) { + socketAddresses.add(new InetSocketAddress(inetAddresses[i], mPort)); + } + } catch (IOException ioe) { + LogUtils.d(TAG, ioe.toString()); + mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_RESOLVE_HOST_ON_NETWORK); + throw new MessagingException(MessagingException.IOERROR, ioe.toString()); + } + } + + boolean success = false; + while (socketAddresses.size() > 0) { + mSocket = createSocket(); + try { + mAddress = socketAddresses.remove(0); + mSocket.connect(mAddress, SOCKET_CONNECT_TIMEOUT); + + if (canTrySslSecurity()) { + /* + SSLSocket cannot be created with a connection timeout, so instead of doing a + direct SSL connection, we connect with a normal connection and upgrade it into + SSL + */ + reopenTls(); + } else { + mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); + mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); + mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); + } + success = true; + return; + } catch (IOException ioe) { + LogUtils.d(TAG, ioe.toString()); + if (socketAddresses.size() == 0) { + // Only throw an error when there are no more sockets to try. + mImapHelper.handleEvent(OmtpEvents.DATA_ALL_SOCKET_CONNECTION_FAILED); + throw new MessagingException(MessagingException.IOERROR, ioe.toString()); + } + } finally { + if (!success) { + try { + mSocket.close(); + mSocket = null; + } catch (IOException ioe) { + throw new MessagingException(MessagingException.IOERROR, ioe.toString()); + } + + } + } + } + } + + // For testing. We need something that can replace the behavior of "new Socket()" + @VisibleForTesting + interface SocketCreator { + + Socket createSocket() throws MessagingException; + } + + @VisibleForTesting + void setSocketCreator(SocketCreator creator) { + mSocketCreator = creator; + } + + protected Socket createSocket() throws MessagingException { + if (mSocketCreator != null) { + return mSocketCreator.createSocket(); + } + + if (mNetwork == null) { + LogUtils.v(TAG, "createSocket: network not specified"); + return new Socket(); + } + + try { + LogUtils.v(TAG, "createSocket: network specified"); + return mNetwork.getSocketFactory().createSocket(); + } catch (IOException ioe) { + LogUtils.d(TAG, ioe.toString()); + throw new MessagingException(MessagingException.IOERROR, ioe.toString()); + } + } + + /** + * Attempts to reopen a normal connection into a TLS connection. + */ + public void reopenTls() throws MessagingException { + try { + LogUtils.d(TAG, "open: converting to TLS socket"); + mSocket = HttpsURLConnection.getDefaultSSLSocketFactory() + .createSocket(mSocket, mAddress.getHostName(), mAddress.getPort(), true); + // After the socket connects to an SSL server, confirm that the hostname is as + // expected + if (!canTrustAllCertificates()) { + verifyHostname(mSocket, mHost); + } + mSocket.setSoTimeout(SOCKET_READ_TIMEOUT); + mIn = new BufferedInputStream(mSocket.getInputStream(), 1024); + mOut = new BufferedOutputStream(mSocket.getOutputStream(), 512); + + } catch (SSLException e) { + LogUtils.d(TAG, e.toString()); + throw new CertificateValidationException(e.getMessage(), e); + } catch (IOException ioe) { + LogUtils.d(TAG, ioe.toString()); + throw new MessagingException(MessagingException.IOERROR, ioe.toString()); + } + } + + /** + * Lightweight version of SSLCertificateSocketFactory.verifyHostname, which provides this + * service but is not in the public API. + * + * Verify the hostname of the certificate used by the other end of a + * connected socket. It is harmless to call this method redundantly if the hostname has already + * been verified. + * + * <p>Wildcard certificates are allowed to verify any matching hostname, + * so "foo.bar.example.com" is verified if the peer has a certificate + * for "*.example.com". + * + * @param socket An SSL socket which has been connected to a server + * @param hostname The expected hostname of the remote server + * @throws IOException if something goes wrong handshaking with the server + * @throws SSLPeerUnverifiedException if the server cannot prove its identity + */ + private void verifyHostname(Socket socket, String hostname) throws IOException { + // The code at the start of OpenSSLSocketImpl.startHandshake() + // ensures that the call is idempotent, so we can safely call it. + SSLSocket ssl = (SSLSocket) socket; + ssl.startHandshake(); + + SSLSession session = ssl.getSession(); + if (session == null) { + mImapHelper.handleEvent(OmtpEvents.DATA_CANNOT_ESTABLISH_SSL_SESSION); + throw new SSLException("Cannot verify SSL socket without session"); + } + // TODO: Instead of reporting the name of the server we think we're connecting to, + // we should be reporting the bad name in the certificate. Unfortunately this is buried + // in the verifier code and is not available in the verifier API, and extracting the + // CN & alts is beyond the scope of this patch. + if (!HOSTNAME_VERIFIER.verify(hostname, session)) { + mImapHelper.handleEvent(OmtpEvents.DATA_SSL_INVALID_HOST_NAME); + throw new SSLPeerUnverifiedException("Certificate hostname not useable for server: " + + session.getPeerPrincipal()); + } + } + + public boolean isOpen() { + return (mIn != null && mOut != null && + mSocket != null && mSocket.isConnected() && !mSocket.isClosed()); + } + + /** + * Close the connection. MUST NOT return any exceptions - must be "best effort" and safe. + */ + public void close() { + 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; + } + + public String getHost() { + return mHost; + } + + public InputStream getInputStream() { + return mIn; + } + + public OutputStream getOutputStream() { + return mOut; + } + + /** + * Writes a single line to the server using \r\n termination. + */ + public void writeLine(String s, String sensitiveReplacement) throws IOException { + if (sensitiveReplacement != null) { + LogUtils.d(TAG, ">>> " + sensitiveReplacement); + } else { + LogUtils.d(TAG, ">>> " + s); + } + + OutputStream out = getOutputStream(); + out.write(s.getBytes()); + out.write('\r'); + out.write('\n'); + out.flush(); + } + + /** + * Reads a single line from the server, using either \r\n or \n as the delimiter. The + * delimiter char(s) are not included in the result. + */ + public String readLine(boolean loggable) throws IOException { + StringBuffer sb = new StringBuffer(); + InputStream in = getInputStream(); + int d; + while ((d = in.read()) != -1) { + if (((char)d) == '\r') { + continue; + } else if (((char)d) == '\n') { + break; + } else { + sb.append((char)d); + } + } + if (d == -1) { + LogUtils.d(TAG, "End of stream reached while trying to read line."); + } + String ret = sb.toString(); + if (loggable) { + LogUtils.d(TAG, "<<< " + ret); + } + return ret; + } +} diff --git a/java/com/android/voicemailomtp/mail/MeetingInfo.java b/java/com/android/voicemailomtp/mail/MeetingInfo.java new file mode 100644 index 000000000..0505bbf2c --- /dev/null +++ b/java/com/android/voicemailomtp/mail/MeetingInfo.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +public class MeetingInfo { + // Predefined tags; others can be added + public static final String MEETING_DTSTAMP = "DTSTAMP"; + public static final String MEETING_UID = "UID"; + public static final String MEETING_ORGANIZER_EMAIL = "ORGMAIL"; + public static final String MEETING_DTSTART = "DTSTART"; + public static final String MEETING_DTEND = "DTEND"; + public static final String MEETING_TITLE = "TITLE"; + public static final String MEETING_LOCATION = "LOC"; + public static final String MEETING_RESPONSE_REQUESTED = "RESPONSE"; + public static final String MEETING_ALL_DAY = "ALLDAY"; +} diff --git a/java/com/android/voicemailomtp/mail/Message.java b/java/com/android/voicemailomtp/mail/Message.java new file mode 100644 index 000000000..41555690f --- /dev/null +++ b/java/com/android/voicemailomtp/mail/Message.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +import android.support.annotation.VisibleForTesting; +import java.util.Date; +import java.util.HashSet; + +public abstract class Message implements Part, Body { + public static final Message[] EMPTY_ARRAY = new Message[0]; + + public static final String RECIPIENT_TYPE_TO = "to"; + public static final String RECIPIENT_TYPE_CC = "cc"; + public static final String RECIPIENT_TYPE_BCC = "bcc"; + public enum RecipientType { + TO, CC, BCC, + } + + protected String mUid; + + private HashSet<String> mFlags = null; + + protected Date mInternalDate; + + public String getUid() { + return mUid; + } + + public void setUid(String uid) { + this.mUid = uid; + } + + 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(String type) throws MessagingException; + + public abstract void setRecipients(String type, Address[] addresses) + throws MessagingException; + + public void setRecipient(String 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; + + // Always use these instead of getHeader("Message-ID") or setHeader("Message-ID"); + public abstract void setMessageId(String messageId) throws MessagingException; + public abstract String getMessageId() throws MessagingException; + + @Override + public boolean isMimeType(String mimeType) throws MessagingException { + return getContentType().startsWith(mimeType); + } + + private HashSet<String> getFlagSet() { + if (mFlags == null) { + mFlags = new HashSet<String>(); + } + return mFlags; + } + + /* + * TODO Refactor Flags at some point to be able to store user defined flags. + */ + public String[] getFlags() { + return getFlagSet().toArray(new String[] {}); + } + + /** + * Set/clear a flag directly, without involving overrides of {@link #setFlag} in subclasses. + * Only used for testing. + */ + @VisibleForTesting + private final void setFlagDirectlyForTest(String flag, boolean set) throws MessagingException { + if (set) { + getFlagSet().add(flag); + } else { + getFlagSet().remove(flag); + } + } + + public void setFlag(String flag, boolean set) throws MessagingException { + setFlagDirectlyForTest(flag, set); + } + + /** + * This method calls setFlag(String, boolean) + * @param flags + * @param set + */ + public void setFlags(String[] flags, boolean set) throws MessagingException { + for (String flag : flags) { + setFlag(flag, set); + } + } + + public boolean isSet(String flag) { + return getFlagSet().contains(flag); + } + + public abstract void saveChanges() throws MessagingException; + + @Override + public String toString() { + return getClass().getSimpleName() + ':' + mUid; + } +} diff --git a/java/com/android/voicemailomtp/mail/MessageDateComparator.java b/java/com/android/voicemailomtp/mail/MessageDateComparator.java new file mode 100644 index 000000000..37071034a --- /dev/null +++ b/java/com/android/voicemailomtp/mail/MessageDateComparator.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +import java.util.Comparator; + +public class MessageDateComparator implements Comparator<Message> { + @Override + 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; + } + } +} diff --git a/java/com/android/voicemailomtp/mail/MessagingException.java b/java/com/android/voicemailomtp/mail/MessagingException.java new file mode 100644 index 000000000..28550527f --- /dev/null +++ b/java/com/android/voicemailomtp/mail/MessagingException.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +/** + * This exception is used for most types of failures that occur during server interactions. + * + * Data passed through this exception should be considered non-localized. Any strings should + * either be internal-only (for debugging) or server-generated. + * + * TO DO: Does it make sense to further collapse AuthenticationFailedException and + * CertificateValidationException and any others into this? + */ +public class MessagingException extends Exception { + public static final long serialVersionUID = -1; + + public static final int NO_ERROR = -1; + /** Any exception that does not specify a specific issue */ + public static final int UNSPECIFIED_EXCEPTION = 0; + /** Connection or IO errors */ + public static final int IOERROR = 1; + /** The configuration requested TLS but the server did not support it. */ + public static final int TLS_REQUIRED = 2; + /** Authentication is required but the server did not support it. */ + public static final int AUTH_REQUIRED = 3; + /** General security failures */ + public static final int GENERAL_SECURITY = 4; + /** Authentication failed */ + public static final int AUTHENTICATION_FAILED = 5; + /** Attempt to create duplicate account */ + public static final int DUPLICATE_ACCOUNT = 6; + /** Required security policies reported - advisory only */ + public static final int SECURITY_POLICIES_REQUIRED = 7; + /** Required security policies not supported */ + public static final int SECURITY_POLICIES_UNSUPPORTED = 8; + /** The protocol (or protocol version) isn't supported */ + public static final int PROTOCOL_VERSION_UNSUPPORTED = 9; + /** The server's SSL certificate couldn't be validated */ + public static final int CERTIFICATE_VALIDATION_ERROR = 10; + /** Authentication failed during autodiscover */ + public static final int AUTODISCOVER_AUTHENTICATION_FAILED = 11; + /** Autodiscover completed with a result (non-error) */ + public static final int AUTODISCOVER_AUTHENTICATION_RESULT = 12; + /** Ambiguous failure; server error or bad credentials */ + public static final int AUTHENTICATION_FAILED_OR_SERVER_ERROR = 13; + /** The server refused access */ + public static final int ACCESS_DENIED = 14; + /** The server refused access */ + public static final int ATTACHMENT_NOT_FOUND = 15; + /** A client SSL certificate is required for connections to the server */ + public static final int CLIENT_CERTIFICATE_REQUIRED = 16; + /** The client SSL certificate specified is invalid */ + public static final int CLIENT_CERTIFICATE_ERROR = 17; + /** The server indicates it does not support OAuth authentication */ + public static final int OAUTH_NOT_SUPPORTED = 18; + /** The server indicates it experienced an internal error */ + public static final int SERVER_ERROR = 19; + + protected int mExceptionType; + // Exception type-specific data + protected Object mExceptionData; + + public MessagingException(String message, Throwable throwable) { + this(UNSPECIFIED_EXCEPTION, message, throwable); + } + + public MessagingException(int exceptionType, String message, Throwable throwable) { + super(message, throwable); + mExceptionType = exceptionType; + mExceptionData = null; + } + + /** + * Constructs a MessagingException with an exceptionType and a null message. + * @param exceptionType The exception type to set for this exception. + */ + public MessagingException(int exceptionType) { + this(exceptionType, null, null); + } + + /** + * Constructs a MessagingException with a message. + * @param message the message for this exception + */ + public MessagingException(String message) { + this(UNSPECIFIED_EXCEPTION, message, null); + } + + /** + * Constructs a MessagingException with an exceptionType and a message. + * @param exceptionType The exception type to set for this exception. + */ + public MessagingException(int exceptionType, String message) { + this(exceptionType, message, null); + } + + /** + * Constructs a MessagingException with an exceptionType, a message, and data + * @param exceptionType The exception type to set for this exception. + * @param message the message for the exception (or null) + * @param data exception-type specific data for the exception (or null) + */ + public MessagingException(int exceptionType, String message, Object data) { + super(message); + mExceptionType = exceptionType; + mExceptionData = data; + } + + /** + * Return the exception type. Will be OTHER_EXCEPTION if not explicitly set. + * + * @return Returns the exception type. + */ + public int getExceptionType() { + return mExceptionType; + } + /** + * Return the exception data. Will be null if not explicitly set. + * + * @return Returns the exception data. + */ + public Object getExceptionData() { + return mExceptionData; + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/Multipart.java b/java/com/android/voicemailomtp/mail/Multipart.java new file mode 100644 index 000000000..b45ebab3d --- /dev/null +++ b/java/com/android/voicemailomtp/mail/Multipart.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.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; + } +} diff --git a/java/com/android/voicemailomtp/mail/PackedString.java b/java/com/android/voicemailomtp/mail/PackedString.java new file mode 100644 index 000000000..585759611 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/PackedString.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +import java.util.HashMap; +import java.util.Map; + +/** + * A utility class for creating and modifying Strings that are tagged and packed together. + * + * Uses non-printable (control chars) for internal delimiters; Intended for regular displayable + * strings only, so please use base64 or other encoding if you need to hide any binary data here. + * + * Binary compatible with Address.pack() format, which should migrate to use this code. + */ +public class PackedString { + + /** + * Packing format is: + * element : [ value ] or [ value TAG-DELIMITER tag ] + * packed-string : [ element ] [ ELEMENT-DELIMITER [ element ] ]* + */ + private static final char DELIMITER_ELEMENT = '\1'; + private static final char DELIMITER_TAG = '\2'; + + private String mString; + private HashMap<String, String> mExploded; + private static final HashMap<String, String> EMPTY_MAP = new HashMap<String, String>(); + + /** + * Create a packed string using an already-packed string (e.g. from database) + * @param string packed string + */ + public PackedString(String string) { + mString = string; + mExploded = null; + } + + /** + * Get the value referred to by a given tag. If the tag does not exist, return null. + * @param tag identifier of string of interest + * @return returns value, or null if no string is found + */ + public String get(String tag) { + if (mExploded == null) { + mExploded = explode(mString); + } + return mExploded.get(tag); + } + + /** + * Return a map of all of the values referred to by a given tag. This is a shallow + * copy, don't edit the values. + * @return a map of the values in the packed string + */ + public Map<String, String> unpack() { + if (mExploded == null) { + mExploded = explode(mString); + } + return new HashMap<String,String>(mExploded); + } + + /** + * Read out all values into a map. + */ + private static HashMap<String, String> explode(String packed) { + if (packed == null || packed.length() == 0) { + return EMPTY_MAP; + } + HashMap<String, String> map = new HashMap<String, String>(); + + int length = packed.length(); + int elementStartIndex = 0; + int elementEndIndex = 0; + int tagEndIndex = packed.indexOf(DELIMITER_TAG); + + while (elementStartIndex < length) { + elementEndIndex = packed.indexOf(DELIMITER_ELEMENT, elementStartIndex); + if (elementEndIndex == -1) { + elementEndIndex = length; + } + String tag; + String value; + if (tagEndIndex == -1 || elementEndIndex <= tagEndIndex) { + // in this case the DELIMITER_PERSONAL is in a future pair (or not found) + // so synthesize a positional tag for the value, and don't update tagEndIndex + value = packed.substring(elementStartIndex, elementEndIndex); + tag = Integer.toString(map.size()); + } else { + value = packed.substring(elementStartIndex, tagEndIndex); + tag = packed.substring(tagEndIndex + 1, elementEndIndex); + // scan forward for next tag, if any + tagEndIndex = packed.indexOf(DELIMITER_TAG, elementEndIndex + 1); + } + map.put(tag, value); + elementStartIndex = elementEndIndex + 1; + } + + return map; + } + + /** + * Builder class for creating PackedString values. Can also be used for editing existing + * PackedString representations. + */ + static public class Builder { + HashMap<String, String> mMap; + + /** + * Create a builder that's empty (for filling) + */ + public Builder() { + mMap = new HashMap<String, String>(); + } + + /** + * Create a builder using the values of an existing PackedString (for editing). + */ + public Builder(String packed) { + mMap = explode(packed); + } + + /** + * Add a tagged value + * @param tag identifier of string of interest + * @param value the value to record in this position. null to delete entry. + */ + public void put(String tag, String value) { + if (value == null) { + mMap.remove(tag); + } else { + mMap.put(tag, value); + } + } + + /** + * Get the value referred to by a given tag. If the tag does not exist, return null. + * @param tag identifier of string of interest + * @return returns value, or null if no string is found + */ + public String get(String tag) { + return mMap.get(tag); + } + + /** + * Pack the values and return a single, encoded string + */ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (Map.Entry<String,String> entry : mMap.entrySet()) { + if (sb.length() > 0) { + sb.append(DELIMITER_ELEMENT); + } + sb.append(entry.getValue()); + sb.append(DELIMITER_TAG); + sb.append(entry.getKey()); + } + return sb.toString(); + } + } +} diff --git a/java/com/android/voicemailomtp/mail/Part.java b/java/com/android/voicemailomtp/mail/Part.java new file mode 100644 index 000000000..51f8a4c38 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/Part.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +import java.io.IOException; +import java.io.OutputStream; + +public interface Part extends Fetchable { + 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 getContentId() throws MessagingException; + + public String[] getHeader(String name) throws MessagingException; + + public void setExtendedHeader(String name, String value) throws MessagingException; + + public String getExtendedHeader(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; +} diff --git a/java/com/android/voicemailomtp/mail/PeekableInputStream.java b/java/com/android/voicemailomtp/mail/PeekableInputStream.java new file mode 100644 index 000000000..c1181d189 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/PeekableInputStream.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +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 final 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); + } + + @Override + public String toString() { + return String.format("PeekableInputStream(in=%s, peeked=%b, peekedByte=%d)", + mIn.toString(), mPeeked, mPeekedByte); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/TempDirectory.java b/java/com/android/voicemailomtp/mail/TempDirectory.java new file mode 100644 index 000000000..dfae36026 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/TempDirectory.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail; + +import android.content.Context; + +import java.io.File; + +/** + * TempDirectory caches the directory used for caching file. It is set up during application + * initialization. + */ +public class TempDirectory { + private static File sTempDirectory = null; + + public static void setTempDirectory(Context context) { + sTempDirectory = context.getCacheDir(); + } + + public static File getTempDirectory() { + if (sTempDirectory == null) { + throw new RuntimeException( + "TempDirectory not set. " + + "If in a unit test, call Email.setTempDirectory(context) in setUp()."); + } + return sTempDirectory; + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java b/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java new file mode 100644 index 000000000..52c43de16 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.internet; + +import com.android.voicemailomtp.mail.Body; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.mail.TempDirectory; + +import org.apache.commons.io.IOUtils; + +import android.util.Base64; +import android.util.Base64OutputStream; + +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; + +/** + * 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 File mFile; + + /** + * An alternate way to put data into a BinaryTempFileBody is to simply supply an already- + * created file. Note that this file will be deleted after it is read. + * @param filePath The file containing the data to be stored on disk temporarily + */ + public void setFile(String filePath) { + mFile = new File(filePath); + } + + public OutputStream getOutputStream() throws IOException { + mFile = File.createTempFile("body", null, TempDirectory.getTempDirectory()); + mFile.deleteOnExit(); + return new FileOutputStream(mFile); + } + + @Override + public InputStream getInputStream() throws MessagingException { + try { + return new BinaryTempFileBodyInputStream(new FileInputStream(mFile)); + } + catch (IOException ioe) { + throw new MessagingException("Unable to open body", ioe); + } + } + + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + InputStream in = getInputStream(); + Base64OutputStream base64Out = new Base64OutputStream( + out, Base64.CRLF | Base64.NO_CLOSE); + IOUtils.copy(in, base64Out); + base64Out.close(); + mFile.delete(); + in.close(); + } + + class BinaryTempFileBodyInputStream extends FilterInputStream { + public BinaryTempFileBodyInputStream(InputStream in) { + super(in); + } + + @Override + public void close() throws IOException { + super.close(); + mFile.delete(); + } + } +} diff --git a/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java b/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java new file mode 100644 index 000000000..8a9c45cf9 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.internet; + +import com.android.voicemailomtp.mail.Body; +import com.android.voicemailomtp.mail.BodyPart; +import com.android.voicemailomtp.mail.MessagingException; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.regex.Pattern; + +/** + * 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 MimeHeader mExtendedHeader; + protected Body mBody; + protected int mSize; + + // regex that matches content id surrounded by "<>" optionally. + private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$"); + // regex that matches end of line. + private static final Pattern END_OF_LINE = Pattern.compile("\r?\n"); + + 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); + } + + @Override + public void addHeader(String name, String value) throws MessagingException { + mHeader.addHeader(name, value); + } + + @Override + public void setHeader(String name, String value) throws MessagingException { + mHeader.setHeader(name, value); + } + + @Override + public String[] getHeader(String name) throws MessagingException { + return mHeader.getHeader(name); + } + + @Override + public void removeHeader(String name) throws MessagingException { + mHeader.removeHeader(name); + } + + @Override + public Body getBody() throws MessagingException { + return mBody; + } + + @Override + public void setBody(Body body) throws MessagingException { + this.mBody = body; + if (body instanceof com.android.voicemailomtp.mail.Multipart) { + com.android.voicemailomtp.mail.Multipart multipart = + ((com.android.voicemailomtp.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"); + } + } + + @Override + public String getContentType() throws MessagingException { + String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); + if (contentType == null) { + return "text/plain"; + } else { + return contentType; + } + } + + @Override + public String getDisposition() throws MessagingException { + String contentDisposition = getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION); + if (contentDisposition == null) { + return null; + } else { + return contentDisposition; + } + } + + @Override + public String getContentId() throws MessagingException { + String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID); + if (contentId == null) { + return null; + } else { + // remove optionally surrounding brackets. + return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1"); + } + } + + @Override + public String getMimeType() throws MessagingException { + return MimeUtility.getHeaderParameter(getContentType(), null); + } + + @Override + public boolean isMimeType(String mimeType) throws MessagingException { + return getMimeType().equals(mimeType); + } + + public void setSize(int size) { + this.mSize = size; + } + + @Override + public int getSize() throws MessagingException { + return mSize; + } + + /** + * Set extended header + * + * @param name Extended header name + * @param value header value - flattened by removing CR-NL if any + * remove header if value is null + * @throws MessagingException + */ + @Override + public void setExtendedHeader(String name, String value) throws MessagingException { + if (value == null) { + if (mExtendedHeader != null) { + mExtendedHeader.removeHeader(name); + } + return; + } + if (mExtendedHeader == null) { + mExtendedHeader = new MimeHeader(); + } + mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll("")); + } + + /** + * Get extended header + * + * @param name Extended header name + * @return header value - null if header does not exist + * @throws MessagingException + */ + @Override + public String getExtendedHeader(String name) throws MessagingException { + if (mExtendedHeader == null) { + return null; + } + return mExtendedHeader.getFirstHeader(name); + } + + /** + * Write the MimeMessage out in MIME format. + */ + @Override + 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); + } + } +} diff --git a/java/com/android/voicemailomtp/mail/internet/MimeHeader.java b/java/com/android/voicemailomtp/mail/internet/MimeHeader.java new file mode 100644 index 000000000..4b0aea749 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/internet/MimeHeader.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.internet; + +import com.android.voicemailomtp.mail.MessagingException; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.util.ArrayList; + +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.appendMessage 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"; + public static final String HEADER_CONTENT_ID = "Content-ID"; + + /** + * Fields that should be omitted when writing the header using writeTo() + */ + private static final String[] WRITE_OMIT_FIELDS = { +// HEADER_ANDROID_ATTACHMENT_DOWNLOADED, +// HEADER_ANDROID_ATTACHMENT_ID, + HEADER_ANDROID_ATTACHMENT_STORE_DATA + }; + + protected final 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, 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); + } + + /** + * Write header into String + * + * @return CR-NL separated header string except the headers in writeOmitFields + * null if header is empty + */ + public String writeToString() { + if (mFields.size() == 0) { + return null; + } + StringBuilder builder = new StringBuilder(); + for (Field field : mFields) { + if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) { + builder.append(field.name + ": " + field.value + "\r\n"); + } + } + return builder.toString(); + } + + public void writeTo(OutputStream out) throws IOException, MessagingException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); + for (Field field : mFields) { + if (!arrayContains(WRITE_OMIT_FIELDS, field.name)) { + writer.write(field.name + ": " + field.value + "\r\n"); + } + } + writer.flush(); + } + + private static class Field { + final String name; + final String value; + + public Field(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public String toString() { + return name + "=" + value; + } + } + + @Override + public String toString() { + return (mFields == null) ? null : mFields.toString(); + } + + public final static boolean arrayContains(Object[] a, Object o) { + int index = arrayIndex(a, o); + return (index >= 0); + } + + public final static int arrayIndex(Object[] a, Object o) { + for (int i = 0, count = a.length; i < count; i++) { + if (a[i].equals(o)) { + return i; + } + } + return -1; + } +} diff --git a/java/com/android/voicemailomtp/mail/internet/MimeMessage.java b/java/com/android/voicemailomtp/mail/internet/MimeMessage.java new file mode 100644 index 000000000..a11cd6d83 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/internet/MimeMessage.java @@ -0,0 +1,675 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.internet; + +import com.android.voicemailomtp.mail.Address; +import com.android.voicemailomtp.mail.Body; +import com.android.voicemailomtp.mail.BodyPart; +import com.android.voicemailomtp.mail.Message; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.mail.Multipart; +import com.android.voicemailomtp.mail.Part; +import com.android.voicemailomtp.mail.utils.LogUtils; + +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 android.text.TextUtils; + +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.Locale; +import java.util.Stack; +import java.util.regex.Pattern; + +/** + * An implementation of Message that stores all of its metadata in RFC 822 and + * RFC 2045 style headers. + * + * NOTE: Automatic generation of a local message-id is becoming unwieldy and should be removed. + * It would be better to simply do it explicitly on local creation of new outgoing messages. + */ +public class MimeMessage extends Message { + private MimeHeader mHeader; + private MimeHeader mExtendedHeader; + + // NOTE: The fields here are transcribed out of headers, and values stored here will supersede + // the values found in the headers. Use caution to prevent any out-of-phase errors. In + // particular, any adds/changes/deletes here must be echoed by changes in the parse() function. + private Address[] mFrom; + private Address[] mTo; + private Address[] mCc; + private Address[] mBcc; + private Address[] mReplyTo; + private Date mSentDate; + private Body mBody; + protected int mSize; + private boolean mInhibitLocalMessageId = false; + private boolean mComplete = true; + + // Shared random source for generating local message-id values + private static final java.util.Random sRandom = new java.util.Random(); + + // In MIME, en_US-like date format should be used. In other words "MMM" should be encoded to + // "Jan", not the other localized format like "Ene" (meaning January in locale es). + // This conversion is used when generating outgoing MIME messages. Incoming MIME date + // headers are parsed by org.apache.james.mime4j.field.DateTimeField which does not have any + // localization code. + private static final SimpleDateFormat DATE_FORMAT = + new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US); + + // regex that matches content id surrounded by "<>" optionally. + private static final Pattern REMOVE_OPTIONAL_BRACKETS = Pattern.compile("^<?([^>]+)>?$"); + // regex that matches end of line. + private static final Pattern END_OF_LINE = Pattern.compile("\r?\n"); + + public MimeMessage() { + mHeader = null; + } + + /** + * Generate a local message id. This is only used when none has been assigned, and is + * installed lazily. Any remote (typically server-assigned) message id takes precedence. + * @return a long, locally-generated message-ID value + */ + private static String generateMessageId() { + final StringBuilder sb = new StringBuilder(); + sb.append("<"); + for (int i = 0; i < 24; i++) { + // We'll use a 5-bit range (0..31) + final int value = sRandom.nextInt() & 31; + final char c = "0123456789abcdefghijklmnopqrstuv".charAt(value); + sb.append(c); + } + 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 InputStream providing message content + * @throws IOException + * @throws MessagingException + */ + public MimeMessage(InputStream in) throws IOException, MessagingException { + parse(in); + } + + private MimeStreamParser init() { + // Before parsing the input stream, clear all local fields that may be superceded by + // the new incoming message. + getMimeHeaders().clear(); + mInhibitLocalMessageId = true; + mFrom = null; + mTo = null; + mCc = null; + mBcc = null; + mReplyTo = null; + mSentDate = null; + mBody = null; + + final MimeStreamParser parser = new MimeStreamParser(); + parser.setContentHandler(new MimeMessageBuilder()); + return parser; + } + + protected void parse(InputStream in) throws IOException, MessagingException { + final MimeStreamParser parser = init(); + parser.parse(new EOLConvertingInputStream(in)); + mComplete = !parser.getPrematureEof(); + } + + public void parse(InputStream in, EOLConvertingInputStream.Callback callback) + throws IOException, MessagingException { + final MimeStreamParser parser = init(); + parser.parse(new EOLConvertingInputStream(in, getSize(), callback)); + mComplete = !parser.getPrematureEof(); + } + + /** + * Return the internal mHeader value, with very lazy initialization. + * The goal is to save memory by not creating the headers until needed. + */ + private MimeHeader getMimeHeaders() { + if (mHeader == null) { + mHeader = new MimeHeader(); + } + return mHeader; + } + + @Override + public Date getReceivedDate() throws MessagingException { + return null; + } + + @Override + public Date getSentDate() throws MessagingException { + if (mSentDate == null) { + try { + DateTimeField field = (DateTimeField)Field.parse("Date: " + + MimeUtility.unfoldAndDecode(getFirstHeader("Date"))); + mSentDate = field.getDate(); + // TODO: We should make it more clear what exceptions can be thrown here, + // and whether they reflect a normal or error condition. + } catch (Exception e) { + LogUtils.v(LogUtils.TAG, "Message missing Date header"); + } + } + if (mSentDate == null) { + // If we still don't have a date, fall back to "Delivery-date" + try { + DateTimeField field = (DateTimeField)Field.parse("Date: " + + MimeUtility.unfoldAndDecode(getFirstHeader("Delivery-date"))); + mSentDate = field.getDate(); + // TODO: We should make it more clear what exceptions can be thrown here, + // and whether they reflect a normal or error condition. + } catch (Exception e) { + LogUtils.v(LogUtils.TAG, "Message also missing Delivery-Date header"); + } + } + return mSentDate; + } + + @Override + public void setSentDate(Date sentDate) throws MessagingException { + setHeader("Date", DATE_FORMAT.format(sentDate)); + this.mSentDate = sentDate; + } + + @Override + public String getContentType() throws MessagingException { + final String contentType = getFirstHeader(MimeHeader.HEADER_CONTENT_TYPE); + if (contentType == null) { + return "text/plain"; + } else { + return contentType; + } + } + + @Override + public String getDisposition() throws MessagingException { + return getFirstHeader(MimeHeader.HEADER_CONTENT_DISPOSITION); + } + + @Override + public String getContentId() throws MessagingException { + final String contentId = getFirstHeader(MimeHeader.HEADER_CONTENT_ID); + if (contentId == null) { + return null; + } else { + // remove optionally surrounding brackets. + return REMOVE_OPTIONAL_BRACKETS.matcher(contentId).replaceAll("$1"); + } + } + + public boolean isComplete() { + return mComplete; + } + + @Override + public String getMimeType() throws MessagingException { + return MimeUtility.getHeaderParameter(getContentType(), null); + } + + @Override + 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. + */ + @Override + public Address[] getRecipients(String type) throws MessagingException { + if (type == RECIPIENT_TYPE_TO) { + if (mTo == null) { + mTo = Address.parse(MimeUtility.unfold(getFirstHeader("To"))); + } + return mTo; + } else if (type == RECIPIENT_TYPE_CC) { + if (mCc == null) { + mCc = Address.parse(MimeUtility.unfold(getFirstHeader("CC"))); + } + return mCc; + } else if (type == RECIPIENT_TYPE_BCC) { + if (mBcc == null) { + mBcc = Address.parse(MimeUtility.unfold(getFirstHeader("BCC"))); + } + return mBcc; + } else { + throw new MessagingException("Unrecognized recipient type."); + } + } + + @Override + public void setRecipients(String type, Address[] addresses) throws MessagingException { + final int TO_LENGTH = 4; // "To: " + final int CC_LENGTH = 4; // "Cc: " + final int BCC_LENGTH = 5; // "Bcc: " + if (type == RECIPIENT_TYPE_TO) { + if (addresses == null || addresses.length == 0) { + removeHeader("To"); + this.mTo = null; + } else { + setHeader("To", MimeUtility.fold(Address.toHeader(addresses), TO_LENGTH)); + this.mTo = addresses; + } + } else if (type == RECIPIENT_TYPE_CC) { + if (addresses == null || addresses.length == 0) { + removeHeader("CC"); + this.mCc = null; + } else { + setHeader("CC", MimeUtility.fold(Address.toHeader(addresses), CC_LENGTH)); + this.mCc = addresses; + } + } else if (type == RECIPIENT_TYPE_BCC) { + if (addresses == null || addresses.length == 0) { + removeHeader("BCC"); + this.mBcc = null; + } else { + setHeader("BCC", MimeUtility.fold(Address.toHeader(addresses), BCC_LENGTH)); + this.mBcc = addresses; + } + } else { + throw new MessagingException("Unrecognized recipient type."); + } + } + + /** + * Returns the unfolded, decoded value of the Subject header. + */ + @Override + public String getSubject() throws MessagingException { + return MimeUtility.unfoldAndDecode(getFirstHeader("Subject")); + } + + @Override + public void setSubject(String subject) throws MessagingException { + final int HEADER_NAME_LENGTH = 9; // "Subject: " + setHeader("Subject", MimeUtility.foldAndEncode2(subject, HEADER_NAME_LENGTH)); + } + + @Override + 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; + } + + @Override + public void setFrom(Address from) throws MessagingException { + final int FROM_LENGTH = 6; // "From: " + if (from != null) { + setHeader("From", MimeUtility.fold(from.toHeader(), FROM_LENGTH)); + this.mFrom = new Address[] { + from + }; + } else { + this.mFrom = null; + } + } + + @Override + public Address[] getReplyTo() throws MessagingException { + if (mReplyTo == null) { + mReplyTo = Address.parse(MimeUtility.unfold(getFirstHeader("Reply-to"))); + } + return mReplyTo; + } + + @Override + public void setReplyTo(Address[] replyTo) throws MessagingException { + final int REPLY_TO_LENGTH = 10; // "Reply-to: " + if (replyTo == null || replyTo.length == 0) { + removeHeader("Reply-to"); + mReplyTo = null; + } else { + setHeader("Reply-to", MimeUtility.fold(Address.toHeader(replyTo), REPLY_TO_LENGTH)); + mReplyTo = replyTo; + } + } + + /** + * Set the mime "Message-ID" header + * @param messageId the new Message-ID value + * @throws MessagingException + */ + @Override + public void setMessageId(String messageId) throws MessagingException { + setHeader("Message-ID", messageId); + } + + /** + * Get the mime "Message-ID" header. This value will be preloaded with a locally-generated + * random ID, if the value has not previously been set. Local generation can be inhibited/ + * overridden by explicitly clearing the headers, removing the message-id header, etc. + * @return the Message-ID header string, or null if explicitly has been set to null + */ + @Override + public String getMessageId() throws MessagingException { + String messageId = getFirstHeader("Message-ID"); + if (messageId == null && !mInhibitLocalMessageId) { + messageId = generateMessageId(); + setMessageId(messageId); + } + return messageId; + } + + @Override + public void saveChanges() throws MessagingException { + throw new MessagingException("saveChanges not yet implemented"); + } + + @Override + public Body getBody() throws MessagingException { + return mBody; + } + + @Override + public void setBody(Body body) throws MessagingException { + this.mBody = body; + if (body instanceof Multipart) { + final Multipart multipart = ((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 getMimeHeaders().getFirstHeader(name); + } + + @Override + public void addHeader(String name, String value) throws MessagingException { + getMimeHeaders().addHeader(name, value); + } + + @Override + public void setHeader(String name, String value) throws MessagingException { + getMimeHeaders().setHeader(name, value); + } + + @Override + public String[] getHeader(String name) throws MessagingException { + return getMimeHeaders().getHeader(name); + } + + @Override + public void removeHeader(String name) throws MessagingException { + getMimeHeaders().removeHeader(name); + if ("Message-ID".equalsIgnoreCase(name)) { + mInhibitLocalMessageId = true; + } + } + + /** + * Set extended header + * + * @param name Extended header name + * @param value header value - flattened by removing CR-NL if any + * remove header if value is null + * @throws MessagingException + */ + @Override + public void setExtendedHeader(String name, String value) throws MessagingException { + if (value == null) { + if (mExtendedHeader != null) { + mExtendedHeader.removeHeader(name); + } + return; + } + if (mExtendedHeader == null) { + mExtendedHeader = new MimeHeader(); + } + mExtendedHeader.setHeader(name, END_OF_LINE.matcher(value).replaceAll("")); + } + + /** + * Get extended header + * + * @param name Extended header name + * @return header value - null if header does not exist + * @throws MessagingException + */ + @Override + public String getExtendedHeader(String name) throws MessagingException { + if (mExtendedHeader == null) { + return null; + } + return mExtendedHeader.getFirstHeader(name); + } + + /** + * Set entire extended headers from String + * + * @param headers Extended header and its value - "CR-NL-separated pairs + * if null or empty, remove entire extended headers + * @throws MessagingException + */ + public void setExtendedHeaders(String headers) throws MessagingException { + if (TextUtils.isEmpty(headers)) { + mExtendedHeader = null; + } else { + mExtendedHeader = new MimeHeader(); + for (final String header : END_OF_LINE.split(headers)) { + final String[] tokens = header.split(":", 2); + if (tokens.length != 2) { + throw new MessagingException("Illegal extended headers: " + headers); + } + mExtendedHeader.setHeader(tokens[0].trim(), tokens[1].trim()); + } + } + } + + /** + * Get entire extended headers as String + * + * @return "CR-NL-separated extended headers - null if extended header does not exist + */ + public String getExtendedHeaders() { + if (mExtendedHeader != null) { + return mExtendedHeader.writeToString(); + } + return null; + } + + /** + * Write message header and body to output stream + * + * @param out Output steam to write message header and body. + */ + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + final BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); + // Force creation of local message-id + getMessageId(); + getMimeHeaders().writeTo(out); + // mExtendedHeader will not be write out to external output stream, + // because it is intended to internal use. + writer.write("\r\n"); + writer.flush(); + if (mBody != null) { + mBody.writeTo(out); + } + } + + @Override + public InputStream getInputStream() throws MessagingException { + return null; + } + + class MimeMessageBuilder implements ContentHandler { + private final Stack<Object> stack = new Stack<Object>(); + + 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() + "'"); + } + } + + @Override + public void startMessage() { + if (stack.isEmpty()) { + stack.push(MimeMessage.this); + } else { + expect(Part.class); + try { + final MimeMessage m = new MimeMessage(); + ((Part)stack.peek()).setBody(m); + stack.push(m); + } catch (MessagingException me) { + throw new Error(me); + } + } + } + + @Override + public void endMessage() { + expect(MimeMessage.class); + stack.pop(); + } + + @Override + public void startHeader() { + expect(Part.class); + } + + @Override + public void field(String fieldData) { + expect(Part.class); + try { + final String[] tokens = fieldData.split(":", 2); + ((Part)stack.peek()).addHeader(tokens[0], tokens[1].trim()); + } catch (MessagingException me) { + throw new Error(me); + } + } + + @Override + public void endHeader() { + expect(Part.class); + } + + @Override + public void startMultipart(BodyDescriptor bd) { + expect(Part.class); + + final Part e = (Part)stack.peek(); + try { + final MimeMultipart multiPart = new MimeMultipart(e.getContentType()); + e.setBody(multiPart); + stack.push(multiPart); + } catch (MessagingException me) { + throw new Error(me); + } + } + + @Override + public void body(BodyDescriptor bd, InputStream in) throws IOException { + expect(Part.class); + final Body body = MimeUtility.decodeBody(in, bd.getTransferEncoding()); + try { + ((Part)stack.peek()).setBody(body); + } catch (MessagingException me) { + throw new Error(me); + } + } + + @Override + public void endMultipart() { + stack.pop(); + } + + @Override + public void startBodyPart() { + expect(MimeMultipart.class); + + try { + final MimeBodyPart bodyPart = new MimeBodyPart(); + ((MimeMultipart)stack.peek()).addBodyPart(bodyPart); + stack.push(bodyPart); + } catch (MessagingException me) { + throw new Error(me); + } + } + + @Override + public void endBodyPart() { + expect(BodyPart.class); + stack.pop(); + } + + @Override + public void epilogue(InputStream is) throws IOException { + expect(MimeMultipart.class); + final StringBuilder sb = new StringBuilder(); + int b; + while ((b = is.read()) != -1) { + sb.append((char)b); + } + // TODO: why is this commented out? + // ((Multipart) stack.peek()).setEpilogue(sb.toString()); + } + + @Override + public void preamble(InputStream is) throws IOException { + expect(MimeMultipart.class); + final StringBuilder sb = new StringBuilder(); + 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); + } + } + + @Override + public void raw(InputStream is) throws IOException { + throw new UnsupportedOperationException("Not supported"); + } + } +} diff --git a/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java b/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java new file mode 100644 index 000000000..111924336 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/internet/MimeMultipart.java @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.internet; + +import com.android.voicemailomtp.mail.BodyPart; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.mail.Multipart; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; + +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; + } + + @Override + 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); + } + + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out), 1024); + + if (mPreamble != null) { + writer.write(mPreamble + "\r\n"); + } + + for (int i = 0, count = mParts.size(); i < count; i++) { + 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(); + } + + @Override + public InputStream getInputStream() throws MessagingException { + return null; + } + + public String getSubTypeForTest() { + return mSubType; + } +} diff --git a/java/com/android/voicemailomtp/mail/internet/MimeUtility.java b/java/com/android/voicemailomtp/mail/internet/MimeUtility.java new file mode 100644 index 000000000..4d310b0f5 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/internet/MimeUtility.java @@ -0,0 +1,416 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.internet; + +import android.text.TextUtils; +import android.util.Base64; +import android.util.Base64DataException; +import android.util.Base64InputStream; + +import com.android.voicemailomtp.mail.Body; +import com.android.voicemailomtp.mail.BodyPart; +import com.android.voicemailomtp.mail.Message; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.mail.Multipart; +import com.android.voicemailomtp.mail.Part; +import com.android.voicemailomtp.VvmLog; + +import org.apache.commons.io.IOUtils; +import org.apache.james.mime4j.codec.EncoderUtil; +import org.apache.james.mime4j.decoder.DecoderUtil; +import org.apache.james.mime4j.decoder.QuotedPrintableInputStream; +import org.apache.james.mime4j.util.CharsetUtil; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MimeUtility { + private static final String LOG_TAG = "Email"; + + public static final String MIME_TYPE_RFC822 = "message/rfc822"; + private final static Pattern PATTERN_CR_OR_LF = Pattern.compile("\r|\n"); + + /** + * Replace sequences of CRLF+WSP with WSP. Tries to preserve original string + * object whenever possible. + */ + public static String unfold(String s) { + if (s == null) { + return null; + } + Matcher patternMatcher = PATTERN_CR_OR_LF.matcher(s); + if (patternMatcher.find()) { + patternMatcher.reset(); + s = patternMatcher.replaceAll(""); + } + return s; + } + + 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 + // NOTE: When this really works, we *must* remove all calls to foldAndEncode2() to prevent + // duplication of encoding. + public static String foldAndEncode(String s) { + return s; + } + + /** + * INTERIM version of foldAndEncode that will be used only by Subject: headers. + * This is safer than implementing foldAndEncode() (see above) and risking unknown damage + * to other headers. + * + * TODO: Copy this code to foldAndEncode(), get rid of this function, confirm all working OK. + * + * @param s original string to encode and fold + * @param usedCharacters number of characters already used up by header name + + * @return the String ready to be transmitted + */ + public static String foldAndEncode2(String s, int usedCharacters) { + // james.mime4j.codec.EncoderUtil.java + // encode: encodeIfNecessary(text, usage, numUsedInHeaderName) + // Usage.TEXT_TOKENlooks like the right thing for subjects + // use WORD_ENTITY for address/names + + String encoded = EncoderUtil.encodeIfNecessary(s, EncoderUtil.Usage.TEXT_TOKEN, + usedCharacters); + + return fold(encoded, usedCharacters); + } + + /** + * INTERIM: From newer version of org.apache.james (but we don't want to import + * the entire MimeUtil class). + * + * Splits the specified string into a multiple-line representation with + * lines no longer than 76 characters (because the line might contain + * encoded words; see <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC + * 2047</a> section 2). If the string contains non-whitespace sequences + * longer than 76 characters a line break is inserted at the whitespace + * character following the sequence resulting in a line longer than 76 + * characters. + * + * @param s + * string to split. + * @param usedCharacters + * number of characters already used up. Usually the number of + * characters for header field name plus colon and one space. + * @return a multiple-line representation of the given string. + */ + public static String fold(String s, int usedCharacters) { + final int maxCharacters = 76; + + final int length = s.length(); + if (usedCharacters + length <= maxCharacters) + return s; + + StringBuilder sb = new StringBuilder(); + + int lastLineBreak = -usedCharacters; + int wspIdx = indexOfWsp(s, 0); + while (true) { + if (wspIdx == length) { + sb.append(s.substring(Math.max(0, lastLineBreak))); + return sb.toString(); + } + + int nextWspIdx = indexOfWsp(s, wspIdx + 1); + + if (nextWspIdx - lastLineBreak > maxCharacters) { + sb.append(s.substring(Math.max(0, lastLineBreak), wspIdx)); + sb.append("\r\n"); + lastLineBreak = wspIdx; + } + + wspIdx = nextWspIdx; + } + } + + /** + * INTERIM: From newer version of org.apache.james (but we don't want to import + * the entire MimeUtil class). + * + * Search for whitespace. + */ + private static int indexOfWsp(String s, int fromIndex) { + final int len = s.length(); + for (int index = fromIndex; index < len; index++) { + char c = s.charAt(index); + if (c == ' ' || c == '\t') + return index; + } + return len; + } + + /** + * 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. + * + * TODO: quite inefficient with the inner trimming & splitting. + * TODO: Also has a latent bug: uses "startsWith" to match the name, which can false-positive. + * TODO: The doc says that for a null name you get the first param, but you get the header. + * Should probably just fix the doc, but if other code assumes that behavior, fix the code. + * TODO: Need to decode %-escaped strings, as in: filename="ab%22d". + * ('+' -> ' ' conversion too? check RFC) + * + * @param header + * @param name + * @return the entire header (if name=null), the found parameter, or null + */ + public static String getHeaderParameter(String header, String name) { + if (header == null) { + return null; + } + String[] parts = unfold(header).split(";"); + if (name == null) { + return parts[0].trim(); + } + String lowerCaseName = name.toLowerCase(); + for (String part : parts) { + if (part.trim().toLowerCase().startsWith(lowerCaseName)) { + String[] parameterParts = part.split("=", 2); + if (parameterParts.length < 2) { + return null; + } + String parameter = parameterParts[1].trim(); + if (parameter.startsWith("\"") && parameter.endsWith("\"")) { + return parameter.substring(1, parameter.length() - 1); + } else { + return parameter; + } + } + } + return null; + } + + /** + * Reads the Part's body and returns a String based on any charset conversion that needed + * to be done. + * @param part The part containing a body + * @return a String containing the converted text in the body, or null if there was no text + * or an error during conversion. + */ + public static String getTextFromPart(Part part) { + 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); + in.close(); + in = null; // we want all of our memory back, and close might not release + + /* + * We've got a text part, so let's see if it needs to be processed further. + */ + String charset = getHeaderParameter(part.getContentType(), "charset"); + if (charset != null) { + /* + * See if there is conversion from the MIME charset to the Java one. + */ + charset = CharsetUtil.toJavaCharset(charset); + } + /* + * No encoding, so use us-ascii, which is the standard. + */ + if (charset == null) { + charset = "ASCII"; + } + /* + * Convert and return as new String + */ + String result = out.toString(charset); + out.close(); + return result; + } + } + + } + catch (OutOfMemoryError oom) { + /* + * 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. + */ + VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + oom.toString()); + } + 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. + */ + VvmLog.e(LOG_TAG, "Unable to getTextFromPart " + e.toString()); + } + return null; + } + + /** + * Returns true if the given mimeType matches the matchAgainst specification. The comparison + * ignores case and the matchAgainst string may include "*" for a wildcard (e.g. "image/*"). + * + * @param mimeType A MIME type to check. + * @param matchAgainst A MIME type to check against. May include wildcards. + * @return true if the mimeType matches + */ + public static boolean mimeTypeMatches(String mimeType, String matchAgainst) { + Pattern p = Pattern.compile(matchAgainst.replaceAll("\\*", "\\.\\*"), + Pattern.CASE_INSENSITIVE); + return p.matcher(mimeType).matches(); + } + + /** + * Returns true if the given mimeType matches any of the matchAgainst specifications. The + * comparison ignores case and the matchAgainst strings may include "*" for a wildcard + * (e.g. "image/*"). + * + * @param mimeType A MIME type to check. + * @param matchAgainst An array of MIME types to check against. May include wildcards. + * @return true if the mimeType matches any of the matchAgainst strings + */ + public static boolean mimeTypeMatches(String mimeType, String[] matchAgainst) { + for (String matchType : matchAgainst) { + if (mimeTypeMatches(mimeType, matchType)) { + return true; + } + } + return false; + } + + /** + * Given an input stream and a transfer encoding, return a wrapped input stream for that + * encoding (or the original if none is required) + * @param in the input stream + * @param contentTransferEncoding the content transfer encoding + * @return a properly wrapped stream + */ + public static InputStream getInputStreamForContentTransferEncoding(InputStream in, + String contentTransferEncoding) { + 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, Base64.DEFAULT); + } + } + return in; + } + + /** + * 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. + */ + in = getInputStreamForContentTransferEncoding(in, contentTransferEncoding); + BinaryTempFileBody tempBody = new BinaryTempFileBody(); + OutputStream out = tempBody.getOutputStream(); + try { + IOUtils.copy(in, out); + } catch (Base64DataException bde) { + // TODO Need to fix this somehow + //String warning = "\n\n" + Email.getMessageDecodeErrorString(); + //out.write(warning.getBytes()); + } finally { + out.close(); + } + return tempBody; + } + + /** + * Recursively scan a Part (usually a Message) and sort out which of its children will be + * "viewable" and which will be attachments. + * + * @param part The part to be broken down + * @param viewables This arraylist will be populated with all parts that appear to be + * the "message" (e.g. text/plain & text/html) + * @param attachments This arraylist will be populated with all parts that appear to be + * attachments (including inlines) + * @throws MessagingException + */ + public static void collectParts(Part part, ArrayList<Part> viewables, + ArrayList<Part> attachments) throws MessagingException { + String disposition = part.getDisposition(); + String dispositionType = MimeUtility.getHeaderParameter(disposition, null); + // If a disposition is not specified, default to "inline" + boolean inline = + TextUtils.isEmpty(dispositionType) || "inline".equalsIgnoreCase(dispositionType); + // The lower-case mime type + String mimeType = part.getMimeType().toLowerCase(); + + if (part.getBody() instanceof Multipart) { + // 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 its pieces recursively. + MimeMultipart mp = (MimeMultipart)part.getBody(); + boolean foundHtml = false; + if (mp.getSubTypeForTest().equals("alternative")) { + for (int i = 0; i < mp.getCount(); i++) { + if (mp.getBodyPart(i).isMimeType("text/html")) { + foundHtml = true; + break; + } + } + } + for (int i = 0; i < mp.getCount(); i++) { + // See if we have text and html + BodyPart bp = mp.getBodyPart(i); + // If there's html, don't bother loading text + if (foundHtml && bp.isMimeType("text/plain")) { + continue; + } + collectParts(bp, viewables, attachments); + } + } else if (part.getBody() instanceof Message) { + // If the part is an embedded message we just continue to process + // it, pulling any viewables or attachments into the running list. + Message message = (Message)part.getBody(); + collectParts(message, viewables, attachments); + } else if (inline && (mimeType.startsWith("text") || (mimeType.startsWith("image")))) { + // We'll treat text and images as viewables + viewables.add(part); + } else { + // Everything else is an attachment. + attachments.add(part); + } + } +} diff --git a/java/com/android/voicemailomtp/mail/internet/TextBody.java b/java/com/android/voicemailomtp/mail/internet/TextBody.java new file mode 100644 index 000000000..578193eff --- /dev/null +++ b/java/com/android/voicemailomtp/mail/internet/TextBody.java @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.internet; + +import android.util.Base64; + +import com.android.voicemailomtp.mail.Body; +import com.android.voicemailomtp.mail.MessagingException; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; + +public class TextBody implements Body { + String mBody; + + public TextBody(String body) { + this.mBody = body; + } + + @Override + public void writeTo(OutputStream out) throws IOException, MessagingException { + byte[] bytes = mBody.getBytes("UTF-8"); + out.write(Base64.encode(bytes, Base64.CRLF)); + } + + /** + * 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. + */ + @Override + public InputStream getInputStream() throws MessagingException { + try { + byte[] b = mBody.getBytes("UTF-8"); + return new ByteArrayInputStream(b); + } + catch (UnsupportedEncodingException usee) { + return null; + } + } +} diff --git a/java/com/android/voicemailomtp/mail/store/ImapConnection.java b/java/com/android/voicemailomtp/mail/store/ImapConnection.java new file mode 100644 index 000000000..61dcf1281 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/ImapConnection.java @@ -0,0 +1,413 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.store; + +import android.util.ArraySet; +import android.util.Base64; +import com.android.voicemailomtp.mail.AuthenticationFailedException; +import com.android.voicemailomtp.mail.CertificateValidationException; +import com.android.voicemailomtp.mail.MailTransport; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.mail.store.ImapStore.ImapException; +import com.android.voicemailomtp.mail.store.imap.DigestMd5Utils; +import com.android.voicemailomtp.mail.store.imap.ImapConstants; +import com.android.voicemailomtp.mail.store.imap.ImapResponse; +import com.android.voicemailomtp.mail.store.imap.ImapResponseParser; +import com.android.voicemailomtp.mail.store.imap.ImapUtility; +import com.android.voicemailomtp.mail.utils.LogUtils; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.VvmLog; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; +import javax.net.ssl.SSLException; + +/** + * A cacheable class that stores the details for a single IMAP connection. + */ +public class ImapConnection { + private final String TAG = "ImapConnection"; + + private String mLoginPhrase; + private ImapStore mImapStore; + private MailTransport mTransport; + private ImapResponseParser mParser; + private Set<String> mCapabilities = new ArraySet<>(); + + static final String IMAP_REDACTED_LOG = "[IMAP command redacted]"; + + /** + * Next tag to use. All connections associated to the same ImapStore instance share the same + * counter to make tests simpler. + * (Some of the tests involve multiple connections but only have a single counter to track the + * tag.) + */ + private final AtomicInteger mNextCommandTag = new AtomicInteger(0); + + ImapConnection(ImapStore store) { + setStore(store); + } + + void setStore(ImapStore store) { + // TODO: maybe we should throw an exception if the connection is not closed here, + // if it's not currently closed, then we won't reopen it, so if the credentials have + // changed, the connection will not be reestablished. + mImapStore = store; + mLoginPhrase = null; + } + + /** + * Generates and returns the phrase to be used for authentication. This will be a LOGIN with + * username and password. + * + * @return the login command string to sent to the IMAP server + */ + String getLoginPhrase() { + if (mLoginPhrase == null) { + if (mImapStore.getUsername() != null && mImapStore.getPassword() != null) { + // build the LOGIN string once (instead of over-and-over again.) + // apply the quoting here around the built-up password + mLoginPhrase = ImapConstants.LOGIN + " " + mImapStore.getUsername() + " " + + ImapUtility.imapQuoted(mImapStore.getPassword()); + } + } + return mLoginPhrase; + } + + public void open() throws IOException, MessagingException { + if (mTransport != null && mTransport.isOpen()) { + return; + } + + try { + // copy configuration into a clean transport, if necessary + if (mTransport == null) { + mTransport = mImapStore.cloneTransport(); + } + + mTransport.open(); + + createParser(); + + // The server should greet us with something like + // * OK IMAP4rev1 Server + // consume the response before doing anything else. + ImapResponse response = mParser.readResponse(false); + if (!response.isOk()) { + mImapStore.getImapHelper() + .handleEvent(OmtpEvents.DATA_INVALID_INITIAL_SERVER_RESPONSE); + throw new MessagingException( + MessagingException.AUTHENTICATION_FAILED_OR_SERVER_ERROR, + "Invalid server initial response"); + } + + queryCapability(); + + maybeDoStartTls(); + + // LOGIN + doLogin(); + } catch (SSLException e) { + LogUtils.d(TAG, "SSLException ", e); + mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_SSL_EXCEPTION); + throw new CertificateValidationException(e.getMessage(), e); + } catch (IOException ioe) { + LogUtils.d(TAG, "IOException", ioe); + mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_IOE_ON_OPEN); + throw ioe; + } finally { + destroyResponses(); + } + } + + void logout() { + try { + sendCommand(ImapConstants.LOGOUT, false); + if (!mParser.readResponse(true).is(0, ImapConstants.BYE)) { + VvmLog.e(TAG, "Server did not respond LOGOUT with BYE"); + } + if (!mParser.readResponse(false).isOk()) { + VvmLog.e(TAG, "Server did not respond OK after LOGOUT"); + } + } catch (IOException | MessagingException e) { + VvmLog.e(TAG, "Error while logging out:" + e); + } + } + + /** + * Closes the connection and releases all resources. This connection can not be used again + * until {@link #setStore(ImapStore)} is called. + */ + void close() { + if (mTransport != null) { + logout(); + mTransport.close(); + mTransport = null; + } + destroyResponses(); + mParser = null; + mImapStore = null; + } + + /** + * Attempts to convert the connection into secure connection. + */ + private void maybeDoStartTls() throws IOException, MessagingException { + // STARTTLS is required in the OMTP standard but not every implementation support it. + // Make sure the server does have this capability + if (hasCapability(ImapConstants.CAPABILITY_STARTTLS)) { + executeSimpleCommand(ImapConstants.STARTTLS); + mTransport.reopenTls(); + createParser(); + // The cached capabilities should be refreshed after TLS is established. + queryCapability(); + } + } + + /** + * Logs into the IMAP server + */ + private void doLogin() throws IOException, MessagingException, AuthenticationFailedException { + try { + if (mCapabilities.contains(ImapConstants.CAPABILITY_AUTH_DIGEST_MD5)) { + doDigestMd5Auth(); + } else { + executeSimpleCommand(getLoginPhrase(), true); + } + } catch (ImapException ie) { + LogUtils.d(TAG, "ImapException", ie); + String status = ie.getStatus(); + String statusMessage = ie.getStatusMessage(); + String alertText = ie.getAlertText(); + + if (ImapConstants.NO.equals(status)) { + switch (statusMessage) { + case ImapConstants.NO_UNKNOWN_USER: + mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_USER); + break; + case ImapConstants.NO_UNKNOWN_CLIENT: + mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_AUTH_UNKNOWN_DEVICE); + break; + case ImapConstants.NO_INVALID_PASSWORD: + mImapStore.getImapHelper() + .handleEvent(OmtpEvents.DATA_AUTH_INVALID_PASSWORD); + break; + case ImapConstants.NO_MAILBOX_NOT_INITIALIZED: + mImapStore.getImapHelper() + .handleEvent(OmtpEvents.DATA_AUTH_MAILBOX_NOT_INITIALIZED); + break; + case ImapConstants.NO_SERVICE_IS_NOT_PROVISIONED: + mImapStore.getImapHelper() + .handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_PROVISIONED); + break; + case ImapConstants.NO_SERVICE_IS_NOT_ACTIVATED: + mImapStore.getImapHelper() + .handleEvent(OmtpEvents.DATA_AUTH_SERVICE_NOT_ACTIVATED); + break; + case ImapConstants.NO_USER_IS_BLOCKED: + mImapStore.getImapHelper() + .handleEvent(OmtpEvents.DATA_AUTH_USER_IS_BLOCKED); + break; + case ImapConstants.NO_APPLICATION_ERROR: + mImapStore.getImapHelper() + .handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE); + default: + mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_BAD_IMAP_CREDENTIAL); + } + throw new AuthenticationFailedException(alertText, ie); + } + + mImapStore.getImapHelper().handleEvent(OmtpEvents.DATA_REJECTED_SERVER_RESPONSE); + throw new MessagingException(alertText, ie); + } + } + + private void doDigestMd5Auth() throws IOException, MessagingException { + + // Initiate the authentication. + // The server will issue us a challenge, asking to run MD5 on the nonce with our password + // and other data, including the cnonce we randomly generated. + // + // C: a AUTHENTICATE DIGEST-MD5 + // S: (BASE64) realm="elwood.innosoft.com",nonce="OA6MG9tEQGm2hh",qop="auth", + // algorithm=md5-sess,charset=utf-8 + List<ImapResponse> responses = executeSimpleCommand( + ImapConstants.AUTHENTICATE + " " + ImapConstants.AUTH_DIGEST_MD5); + String decodedChallenge = decodeBase64(responses.get(0).getStringOrEmpty(0).getString()); + + Map<String, String> challenge = DigestMd5Utils.parseDigestMessage(decodedChallenge); + DigestMd5Utils.Data data = new DigestMd5Utils.Data(mImapStore, mTransport, challenge); + + String response = data.createResponse(); + // Respond to the challenge. If the server accepts it, it will reply a response-auth which + // is the MD5 of our password and the cnonce we've provided, to prove the server does know + // the password. + // + // C: (BASE64) charset=utf-8,username="chris",realm="elwood.innosoft.com", + // nonce="OA6MG9tEQGm2hh",nc=00000001,cnonce="OA6MHXh6VqTrRk", + // digest-uri="imap/elwood.innosoft.com", + // response=d388dad90d4bbd760a152321f2143af7,qop=auth + // S: (BASE64) rspauth=ea40f60335c427b5527b84dbabcdfffd + + responses = executeContinuationResponse(encodeBase64(response), true); + + // Verify response-auth. + // If failed verifyResponseAuth() will throw a MessagingException, terminating the + // connection + String decodedResponseAuth = decodeBase64(responses.get(0).getStringOrEmpty(0).getString()); + data.verifyResponseAuth(decodedResponseAuth); + + // Send a empty response to indicate we've accepted the response-auth + // + // C: (empty) + // S: a OK User logged in + executeContinuationResponse("", false); + + } + + private static String decodeBase64(String string) { + return new String(Base64.decode(string, Base64.DEFAULT)); + } + + private static String encodeBase64(String string) { + return Base64.encodeToString(string.getBytes(), Base64.NO_WRAP); + } + + private void queryCapability() throws IOException, MessagingException { + List<ImapResponse> responses = executeSimpleCommand(ImapConstants.CAPABILITY); + mCapabilities.clear(); + Set<String> disabledCapabilities = mImapStore.getImapHelper().getConfig() + .getDisabledCapabilities(); + for (ImapResponse response : responses) { + if (response.isTagged()) { + continue; + } + for (int i = 0; i < response.size(); i++) { + String capability = response.getStringOrEmpty(i).getString(); + if (disabledCapabilities != null) { + if (!disabledCapabilities.contains(capability)) { + mCapabilities.add(capability); + } + } else { + mCapabilities.add(capability); + } + } + } + + LogUtils.d(TAG, "Capabilities: " + mCapabilities.toString()); + } + + private boolean hasCapability(String capability) { + return mCapabilities.contains(capability); + } + /** + * Create an {@link ImapResponseParser} from {@code mTransport.getInputStream()} and + * set it to {@link #mParser}. + * + * If we already have an {@link ImapResponseParser}, we + * {@link #destroyResponses()} and throw it away. + */ + private void createParser() { + destroyResponses(); + mParser = new ImapResponseParser(mTransport.getInputStream()); + } + + + public void destroyResponses() { + if (mParser != null) { + mParser.destroyResponses(); + } + } + + public ImapResponse readResponse() throws IOException, MessagingException { + return mParser.readResponse(false); + } + + public List<ImapResponse> executeSimpleCommand(String command) + throws IOException, MessagingException{ + return executeSimpleCommand(command, false); + } + + /** + * Send a single command to the server. The command will be preceded by an IMAP command + * tag and followed by \r\n (caller need not supply them). + * Execute a simple command at the server, a simple command being one that is sent in a single + * line of text + * + * @param command the command to send to the server + * @param sensitive whether the command should be redacted in logs (used for login) + * @return a list of ImapResponses + * @throws IOException + * @throws MessagingException + */ + public List<ImapResponse> executeSimpleCommand(String command, boolean sensitive) + throws IOException, MessagingException { + // TODO: It may be nice to catch IOExceptions and close the connection here. + // Currently, we expect callers to do that, but if they fail to we'll be in a broken state. + sendCommand(command, sensitive); + return getCommandResponses(); + } + + public String sendCommand(String command, boolean sensitive) + throws IOException, MessagingException { + open(); + + if (mTransport == null) { + throw new IOException("Null transport"); + } + String tag = Integer.toString(mNextCommandTag.incrementAndGet()); + String commandToSend = tag + " " + command; + mTransport.writeLine(commandToSend, (sensitive ? IMAP_REDACTED_LOG : command)); + return tag; + } + + List<ImapResponse> executeContinuationResponse(String response, boolean sensitive) + throws IOException, MessagingException { + mTransport.writeLine(response, (sensitive ? IMAP_REDACTED_LOG : response)); + return getCommandResponses(); + } + + /** + * Read and return all of the responses from the most recent command sent to the server + * + * @return a list of ImapResponses + * @throws IOException + * @throws MessagingException + */ + List<ImapResponse> getCommandResponses() + throws IOException, MessagingException { + final List<ImapResponse> responses = new ArrayList<ImapResponse>(); + ImapResponse response; + do { + response = mParser.readResponse(false); + responses.add(response); + } while (!(response.isTagged() || response.isContinuationRequest())); + + if (!(response.isOk() || response.isContinuationRequest())) { + final String toString = response.toString(); + final String status = response.getStatusOrEmpty().getString(); + final String statusMessage = response.getStatusResponseTextOrEmpty().getString(); + final String alert = response.getAlertTextOrEmpty().getString(); + final String responseCode = response.getResponseCodeOrEmpty().getString(); + destroyResponses(); + throw new ImapException(toString, status, statusMessage, alert, responseCode); + } + return responses; + } +} diff --git a/java/com/android/voicemailomtp/mail/store/ImapFolder.java b/java/com/android/voicemailomtp/mail/store/ImapFolder.java new file mode 100644 index 000000000..eca349876 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/ImapFolder.java @@ -0,0 +1,784 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.store; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.Base64DataException; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.mail.AuthenticationFailedException; +import com.android.voicemailomtp.mail.Body; +import com.android.voicemailomtp.mail.FetchProfile; +import com.android.voicemailomtp.mail.Flag; +import com.android.voicemailomtp.mail.Message; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.mail.Part; +import com.android.voicemailomtp.mail.internet.BinaryTempFileBody; +import com.android.voicemailomtp.mail.internet.MimeBodyPart; +import com.android.voicemailomtp.mail.internet.MimeHeader; +import com.android.voicemailomtp.mail.internet.MimeMultipart; +import com.android.voicemailomtp.mail.internet.MimeUtility; +import com.android.voicemailomtp.mail.store.ImapStore.ImapException; +import com.android.voicemailomtp.mail.store.ImapStore.ImapMessage; +import com.android.voicemailomtp.mail.store.imap.ImapConstants; +import com.android.voicemailomtp.mail.store.imap.ImapElement; +import com.android.voicemailomtp.mail.store.imap.ImapList; +import com.android.voicemailomtp.mail.store.imap.ImapResponse; +import com.android.voicemailomtp.mail.store.imap.ImapString; +import com.android.voicemailomtp.mail.utils.LogUtils; +import com.android.voicemailomtp.mail.utils.Utility; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; + +public class ImapFolder { + private static final String TAG = "ImapFolder"; + private final static String[] PERMANENT_FLAGS = + { Flag.DELETED, Flag.SEEN, Flag.FLAGGED, Flag.ANSWERED }; + private static final int COPY_BUFFER_SIZE = 16*1024; + + private final ImapStore mStore; + private final String mName; + private int mMessageCount = -1; + private ImapConnection mConnection; + private String mMode; + private boolean mExists; + /** A set of hashes that can be used to track dirtiness */ + Object mHash[]; + + public static final String MODE_READ_ONLY = "mode_read_only"; + public static final String MODE_READ_WRITE = "mode_read_write"; + + public ImapFolder(ImapStore store, String name) { + mStore = store; + mName = name; + } + + /** + * Callback for each message retrieval. + */ + public interface MessageRetrievalListener { + public void messageRetrieved(Message message); + } + + private void destroyResponses() { + if (mConnection != null) { + mConnection.destroyResponses(); + } + } + + public void open(String mode) throws MessagingException { + try { + if (isOpen()) { + throw new AssertionError("Duplicated open on ImapFolder"); + } + synchronized (this) { + mConnection = mStore.getConnection(); + } + // * FLAGS (\Answered \Flagged \Deleted \Seen \Draft NonJunk + // $MDNSent) + // * OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft + // NonJunk $MDNSent \*)] Flags permitted. + // * 23 EXISTS + // * 0 RECENT + // * OK [UIDVALIDITY 1125022061] UIDs valid + // * OK [UIDNEXT 57576] Predicted next UID + // 2 OK [READ-WRITE] Select completed. + try { + doSelect(); + } catch (IOException ioe) { + throw ioExceptionHandler(mConnection, ioe); + } finally { + destroyResponses(); + } + } catch (AuthenticationFailedException e) { + // Don't cache this connection, so we're forced to try connecting/login again + mConnection = null; + close(false); + throw e; + } catch (MessagingException e) { + mExists = false; + close(false); + throw e; + } + } + + public boolean isOpen() { + return mExists && mConnection != null; + } + + public String getMode() { + return mMode; + } + + public void close(boolean expunge) { + if (expunge) { + try { + expunge(); + } catch (MessagingException e) { + LogUtils.e(TAG, e, "Messaging Exception"); + } + } + mMessageCount = -1; + synchronized (this) { + mConnection = null; + } + } + + public int getMessageCount() { + return mMessageCount; + } + + String[] getSearchUids(List<ImapResponse> responses) { + // S: * SEARCH 2 3 6 + final ArrayList<String> uids = new ArrayList<String>(); + for (ImapResponse response : responses) { + if (!response.isDataResponse(0, ImapConstants.SEARCH)) { + continue; + } + // Found SEARCH response data + for (int i = 1; i < response.size(); i++) { + ImapString s = response.getStringOrEmpty(i); + if (s.isString()) { + uids.add(s.getString()); + } + } + } + return uids.toArray(Utility.EMPTY_STRINGS); + } + + @VisibleForTesting + String[] searchForUids(String searchCriteria) throws MessagingException { + checkOpen(); + try { + try { + final String command = ImapConstants.UID_SEARCH + " " + searchCriteria; + final String[] result = getSearchUids(mConnection.executeSimpleCommand(command)); + LogUtils.d(TAG, "searchForUids '" + searchCriteria + "' results: " + + result.length); + return result; + } catch (ImapException me) { + LogUtils.d(TAG, "ImapException in search: " + searchCriteria, me); + return Utility.EMPTY_STRINGS; // Not found + } catch (IOException ioe) { + LogUtils.d(TAG, "IOException in search: " + searchCriteria, ioe); + mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); + throw ioExceptionHandler(mConnection, ioe); + } + } finally { + destroyResponses(); + } + } + + @Nullable + public Message getMessage(String uid) throws MessagingException { + checkOpen(); + + final String[] uids = searchForUids(ImapConstants.UID + " " + uid); + for (int i = 0; i < uids.length; i++) { + if (uids[i].equals(uid)) { + return new ImapMessage(uid, this); + } + } + LogUtils.e(TAG, "UID " + uid + " not found on server"); + return null; + } + + @VisibleForTesting + protected static boolean isAsciiString(String str) { + int len = str.length(); + for (int i = 0; i < len; i++) { + char c = str.charAt(i); + if (c >= 128) return false; + } + return true; + } + + public Message[] getMessages(String[] uids) throws MessagingException { + if (uids == null) { + uids = searchForUids("1:* NOT DELETED"); + } + return getMessagesInternal(uids); + } + + public Message[] getMessagesInternal(String[] uids) { + final ArrayList<Message> messages = new ArrayList<Message>(uids.length); + for (int i = 0; i < uids.length; i++) { + final String uid = uids[i]; + final ImapMessage message = new ImapMessage(uid, this); + messages.add(message); + } + return messages.toArray(Message.EMPTY_ARRAY); + } + + public void fetch(Message[] messages, FetchProfile fp, + MessageRetrievalListener listener) throws MessagingException { + try { + fetchInternal(messages, fp, listener); + } catch (RuntimeException e) { // Probably a parser error. + LogUtils.w(TAG, "Exception detected: " + e.getMessage()); + throw e; + } + } + + public void fetchInternal(Message[] messages, FetchProfile fp, + MessageRetrievalListener listener) throws MessagingException { + if (messages.length == 0) { + return; + } + checkOpen(); + HashMap<String, Message> messageMap = new HashMap<String, Message>(); + for (Message m : messages) { + messageMap.put(m.getUid(), m); + } + + /* + * Figure out what command we are going to run: + * FLAGS - UID FETCH (FLAGS) + * ENVELOPE - UID FETCH (INTERNALDATE UID RFC822.SIZE FLAGS BODY.PEEK[ + * HEADER.FIELDS (date subject from content-type to cc)]) + * STRUCTURE - UID FETCH (BODYSTRUCTURE) + * BODY_SANE - UID FETCH (BODY.PEEK[]<0.N>) where N = max bytes returned + * BODY - UID FETCH (BODY.PEEK[]) + * Part - UID FETCH (BODY.PEEK[ID]) where ID = mime part ID + */ + + final LinkedHashSet<String> fetchFields = new LinkedHashSet<String>(); + + fetchFields.add(ImapConstants.UID); + if (fp.contains(FetchProfile.Item.FLAGS)) { + fetchFields.add(ImapConstants.FLAGS); + } + if (fp.contains(FetchProfile.Item.ENVELOPE)) { + fetchFields.add(ImapConstants.INTERNALDATE); + fetchFields.add(ImapConstants.RFC822_SIZE); + fetchFields.add(ImapConstants.FETCH_FIELD_HEADERS); + } + if (fp.contains(FetchProfile.Item.STRUCTURE)) { + fetchFields.add(ImapConstants.BODYSTRUCTURE); + } + + if (fp.contains(FetchProfile.Item.BODY_SANE)) { + fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_SANE); + } + if (fp.contains(FetchProfile.Item.BODY)) { + fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK); + } + + // TODO Why are we only fetching the first part given? + final Part fetchPart = fp.getFirstPart(); + if (fetchPart != null) { + final String[] partIds = + fetchPart.getHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA); + // TODO Why can a single part have more than one Id? And why should we only fetch + // the first id if there are more than one? + if (partIds != null) { + fetchFields.add(ImapConstants.FETCH_FIELD_BODY_PEEK_BARE + + "[" + partIds[0] + "]"); + } + } + + try { + mConnection.sendCommand(String.format(Locale.US, + ImapConstants.UID_FETCH + " %s (%s)", ImapStore.joinMessageUids(messages), + Utility.combine(fetchFields.toArray(new String[fetchFields.size()]), ' ') + ), false); + ImapResponse response; + do { + response = null; + try { + response = mConnection.readResponse(); + + if (!response.isDataResponse(1, ImapConstants.FETCH)) { + continue; // Ignore + } + final ImapList fetchList = response.getListOrEmpty(2); + final String uid = fetchList.getKeyedStringOrEmpty(ImapConstants.UID) + .getString(); + if (TextUtils.isEmpty(uid)) continue; + + ImapMessage message = (ImapMessage) messageMap.get(uid); + if (message == null) continue; + + if (fp.contains(FetchProfile.Item.FLAGS)) { + final ImapList flags = + fetchList.getKeyedListOrEmpty(ImapConstants.FLAGS); + for (int i = 0, count = flags.size(); i < count; i++) { + final ImapString flag = flags.getStringOrEmpty(i); + if (flag.is(ImapConstants.FLAG_DELETED)) { + message.setFlagInternal(Flag.DELETED, true); + } else if (flag.is(ImapConstants.FLAG_ANSWERED)) { + message.setFlagInternal(Flag.ANSWERED, true); + } else if (flag.is(ImapConstants.FLAG_SEEN)) { + message.setFlagInternal(Flag.SEEN, true); + } else if (flag.is(ImapConstants.FLAG_FLAGGED)) { + message.setFlagInternal(Flag.FLAGGED, true); + } + } + } + if (fp.contains(FetchProfile.Item.ENVELOPE)) { + final Date internalDate = fetchList.getKeyedStringOrEmpty( + ImapConstants.INTERNALDATE).getDateOrNull(); + final int size = fetchList.getKeyedStringOrEmpty( + ImapConstants.RFC822_SIZE).getNumberOrZero(); + final String header = fetchList.getKeyedStringOrEmpty( + ImapConstants.BODY_BRACKET_HEADER, true).getString(); + + message.setInternalDate(internalDate); + message.setSize(size); + message.parse(Utility.streamFromAsciiString(header)); + } + if (fp.contains(FetchProfile.Item.STRUCTURE)) { + ImapList bs = fetchList.getKeyedListOrEmpty( + ImapConstants.BODYSTRUCTURE); + if (!bs.isEmpty()) { + try { + parseBodyStructure(bs, message, ImapConstants.TEXT); + } catch (MessagingException e) { + LogUtils.v(TAG, e, "Error handling message"); + message.setBody(null); + } + } + } + if (fp.contains(FetchProfile.Item.BODY) + || fp.contains(FetchProfile.Item.BODY_SANE)) { + // Body is keyed by "BODY[]...". + // Previously used "BODY[..." but this can be confused with "BODY[HEADER..." + // TODO Should we accept "RFC822" as well?? + ImapString body = fetchList.getKeyedStringOrEmpty("BODY[]", true); + InputStream bodyStream = body.getAsStream(); + message.parse(bodyStream); + } + if (fetchPart != null) { + InputStream bodyStream = + fetchList.getKeyedStringOrEmpty("BODY[", true).getAsStream(); + String encodings[] = fetchPart.getHeader( + MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING); + + String contentTransferEncoding = null; + if (encodings != null && encodings.length > 0) { + contentTransferEncoding = encodings[0]; + } else { + // According to http://tools.ietf.org/html/rfc2045#section-6.1 + // "7bit" is the default. + contentTransferEncoding = "7bit"; + } + + try { + // TODO Don't create 2 temp files. + // decodeBody creates BinaryTempFileBody, but we could avoid this + // if we implement ImapStringBody. + // (We'll need to share a temp file. Protect it with a ref-count.) + message.setBody(decodeBody(mStore.getContext(), bodyStream, + contentTransferEncoding, fetchPart.getSize(), listener)); + } catch(Exception e) { + // TODO: Figure out what kinds of exceptions might actually be thrown + // from here. This blanket catch-all is because we're not sure what to + // do if we don't have a contentTransferEncoding, and we don't have + // time to figure out what exceptions might be thrown. + LogUtils.e(TAG, "Error fetching body %s", e); + } + } + + if (listener != null) { + listener.messageRetrieved(message); + } + } finally { + destroyResponses(); + } + } while (!response.isTagged()); + } catch (IOException ioe) { + mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); + throw ioExceptionHandler(mConnection, ioe); + } + } + + /** + * Removes any content transfer encoding from the stream and returns a Body. + * This code is taken/condensed from MimeUtility.decodeBody + */ + private static Body decodeBody(Context context,InputStream in, String contentTransferEncoding, + int size, MessageRetrievalListener listener) throws IOException { + // Get a properly wrapped input stream + in = MimeUtility.getInputStreamForContentTransferEncoding(in, contentTransferEncoding); + BinaryTempFileBody tempBody = new BinaryTempFileBody(); + OutputStream out = tempBody.getOutputStream(); + try { + byte[] buffer = new byte[COPY_BUFFER_SIZE]; + int n = 0; + int count = 0; + while (-1 != (n = in.read(buffer))) { + out.write(buffer, 0, n); + count += n; + } + } catch (Base64DataException bde) { + String warning = "\n\nThere was an error while decoding the message."; + out.write(warning.getBytes()); + } finally { + out.close(); + } + return tempBody; + } + + public String[] getPermanentFlags() { + return PERMANENT_FLAGS; + } + + /** + * Handle any untagged responses that the caller doesn't care to handle themselves. + * @param responses + */ + private void handleUntaggedResponses(List<ImapResponse> responses) { + for (ImapResponse response : responses) { + handleUntaggedResponse(response); + } + } + + /** + * Handle an untagged response that the caller doesn't care to handle themselves. + * @param response + */ + private void handleUntaggedResponse(ImapResponse response) { + if (response.isDataResponse(1, ImapConstants.EXISTS)) { + mMessageCount = response.getStringOrEmpty(0).getNumberOrZero(); + } + } + + private static void parseBodyStructure(ImapList bs, Part part, String id) + throws MessagingException { + if (bs.getElementOrNone(0).isList()) { + /* + * This is a multipart/* + */ + MimeMultipart mp = new MimeMultipart(); + for (int i = 0, count = bs.size(); i < count; i++) { + ImapElement e = bs.getElementOrNone(i); + if (e.isList()) { + /* + * For each part in the message we're going to add a new BodyPart and parse + * into it. + */ + MimeBodyPart bp = new MimeBodyPart(); + if (id.equals(ImapConstants.TEXT)) { + parseBodyStructure(bs.getListOrEmpty(i), bp, Integer.toString(i + 1)); + + } else { + parseBodyStructure(bs.getListOrEmpty(i), bp, id + "." + (i + 1)); + } + mp.addBodyPart(bp); + + } else { + if (e.isString()) { + mp.setSubType(bs.getStringOrEmpty(i).getString().toLowerCase(Locale.US)); + } + break; // Ignore the rest of the list. + } + } + part.setBody(mp); + } else { + /* + * This is a body. We need to add as much information as we can find out about + * it to the Part. + */ + + /* + body type + body subtype + body parameter parenthesized list + body id + body description + body encoding + body size + */ + + final ImapString type = bs.getStringOrEmpty(0); + final ImapString subType = bs.getStringOrEmpty(1); + final String mimeType = + (type.getString() + "/" + subType.getString()).toLowerCase(Locale.US); + + final ImapList bodyParams = bs.getListOrEmpty(2); + final ImapString cid = bs.getStringOrEmpty(3); + final ImapString encoding = bs.getStringOrEmpty(5); + final int size = bs.getStringOrEmpty(6).getNumberOrZero(); + + if (MimeUtility.mimeTypeMatches(mimeType, MimeUtility.MIME_TYPE_RFC822)) { + // A body type of type MESSAGE and subtype RFC822 + // contains, immediately after the basic fields, the + // envelope structure, body structure, and size in + // text lines of the encapsulated message. + // [MESSAGE, RFC822, [NAME, filename.eml], NIL, NIL, 7BIT, 5974, NIL, + // [INLINE, [FILENAME*0, Fwd: Xxx..., FILENAME*1, filename.eml]], NIL] + /* + * This will be caught by fetch and handled appropriately. + */ + throw new MessagingException("BODYSTRUCTURE " + MimeUtility.MIME_TYPE_RFC822 + + " not yet supported."); + } + + /* + * Set the content type with as much information as we know right now. + */ + final StringBuilder contentType = new StringBuilder(mimeType); + + /* + * If there are body params we might be able to get some more information out + * of them. + */ + for (int i = 1, count = bodyParams.size(); i < count; i += 2) { + + // TODO We need to convert " into %22, but + // because MimeUtility.getHeaderParameter doesn't recognize it, + // we can't fix it for now. + contentType.append(String.format(";\n %s=\"%s\"", + bodyParams.getStringOrEmpty(i - 1).getString(), + bodyParams.getStringOrEmpty(i).getString())); + } + + part.setHeader(MimeHeader.HEADER_CONTENT_TYPE, contentType.toString()); + + // Extension items + final ImapList bodyDisposition; + + if (type.is(ImapConstants.TEXT) && bs.getElementOrNone(9).isList()) { + // If media-type is TEXT, 9th element might be: [body-fld-lines] := number + // So, if it's not a list, use 10th element. + // (Couldn't find evidence in the RFC if it's ALWAYS 10th element.) + bodyDisposition = bs.getListOrEmpty(9); + } else { + bodyDisposition = bs.getListOrEmpty(8); + } + + final StringBuilder contentDisposition = new StringBuilder(); + + if (bodyDisposition.size() > 0) { + final String bodyDisposition0Str = + bodyDisposition.getStringOrEmpty(0).getString().toLowerCase(Locale.US); + if (!TextUtils.isEmpty(bodyDisposition0Str)) { + contentDisposition.append(bodyDisposition0Str); + } + + final ImapList bodyDispositionParams = bodyDisposition.getListOrEmpty(1); + if (!bodyDispositionParams.isEmpty()) { + /* + * If there is body disposition information we can pull some more + * information about the attachment out. + */ + for (int i = 1, count = bodyDispositionParams.size(); i < count; i += 2) { + + // TODO We need to convert " into %22. See above. + contentDisposition.append(String.format(Locale.US, ";\n %s=\"%s\"", + bodyDispositionParams.getStringOrEmpty(i - 1) + .getString().toLowerCase(Locale.US), + bodyDispositionParams.getStringOrEmpty(i).getString())); + } + } + } + + if ((size > 0) + && (MimeUtility.getHeaderParameter(contentDisposition.toString(), "size") + == null)) { + contentDisposition.append(String.format(Locale.US, ";\n size=%d", size)); + } + + if (contentDisposition.length() > 0) { + /* + * Set the content disposition containing at least the size. Attachment + * handling code will use this down the road. + */ + part.setHeader(MimeHeader.HEADER_CONTENT_DISPOSITION, + contentDisposition.toString()); + } + + /* + * Set the Content-Transfer-Encoding header. Attachment code will use this + * to parse the body. + */ + if (!encoding.isEmpty()) { + part.setHeader(MimeHeader.HEADER_CONTENT_TRANSFER_ENCODING, + encoding.getString()); + } + + /* + * Set the Content-ID header. + */ + if (!cid.isEmpty()) { + part.setHeader(MimeHeader.HEADER_CONTENT_ID, cid.getString()); + } + + if (size > 0) { + if (part instanceof ImapMessage) { + ((ImapMessage) part).setSize(size); + } else if (part instanceof MimeBodyPart) { + ((MimeBodyPart) part).setSize(size); + } else { + throw new MessagingException("Unknown part type " + part.toString()); + } + } + part.setHeader(MimeHeader.HEADER_ANDROID_ATTACHMENT_STORE_DATA, id); + } + + } + + public Message[] expunge() throws MessagingException { + checkOpen(); + try { + handleUntaggedResponses(mConnection.executeSimpleCommand(ImapConstants.EXPUNGE)); + } catch (IOException ioe) { + mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); + throw ioExceptionHandler(mConnection, ioe); + } finally { + destroyResponses(); + } + return null; + } + + public void setFlags(Message[] messages, String[] flags, boolean value) + throws MessagingException { + checkOpen(); + + String allFlags = ""; + if (flags.length > 0) { + StringBuilder flagList = new StringBuilder(); + for (int i = 0, count = flags.length; i < count; i++) { + String flag = flags[i]; + if (flag == Flag.SEEN) { + flagList.append(" " + ImapConstants.FLAG_SEEN); + } else if (flag == Flag.DELETED) { + flagList.append(" " + ImapConstants.FLAG_DELETED); + } else if (flag == Flag.FLAGGED) { + flagList.append(" " + ImapConstants.FLAG_FLAGGED); + } else if (flag == Flag.ANSWERED) { + flagList.append(" " + ImapConstants.FLAG_ANSWERED); + } + } + allFlags = flagList.substring(1); + } + try { + mConnection.executeSimpleCommand(String.format(Locale.US, + ImapConstants.UID_STORE + " %s %s" + ImapConstants.FLAGS_SILENT + " (%s)", + ImapStore.joinMessageUids(messages), + value ? "+" : "-", + allFlags)); + + } catch (IOException ioe) { + mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); + throw ioExceptionHandler(mConnection, ioe); + } finally { + destroyResponses(); + } + } + + /** + * Selects the folder for use. Before performing any operations on this folder, it + * must be selected. + */ + private void doSelect() throws IOException, MessagingException { + final List<ImapResponse> responses = mConnection.executeSimpleCommand( + String.format(Locale.US, ImapConstants.SELECT + " \"%s\"", mName)); + + // Assume the folder is opened read-write; unless we are notified otherwise + mMode = MODE_READ_WRITE; + int messageCount = -1; + for (ImapResponse response : responses) { + if (response.isDataResponse(1, ImapConstants.EXISTS)) { + messageCount = response.getStringOrEmpty(0).getNumberOrZero(); + } else if (response.isOk()) { + final ImapString responseCode = response.getResponseCodeOrEmpty(); + if (responseCode.is(ImapConstants.READ_ONLY)) { + mMode = MODE_READ_ONLY; + } else if (responseCode.is(ImapConstants.READ_WRITE)) { + mMode = MODE_READ_WRITE; + } + } else if (response.isTagged()) { // Not OK + mStore.getImapHelper().handleEvent(OmtpEvents.DATA_MAILBOX_OPEN_FAILED); + throw new MessagingException("Can't open mailbox: " + + response.getStatusResponseTextOrEmpty()); + } + } + if (messageCount == -1) { + throw new MessagingException("Did not find message count during select"); + } + mMessageCount = messageCount; + mExists = true; + } + + public class Quota { + + public final int occupied; + public final int total; + + public Quota(int occupied, int total) { + this.occupied = occupied; + this.total = total; + } + } + + public Quota getQuota() throws MessagingException { + try { + final List<ImapResponse> responses = mConnection.executeSimpleCommand( + String.format(Locale.US, ImapConstants.GETQUOTAROOT + " \"%s\"", mName)); + + for (ImapResponse response : responses) { + if (!response.isDataResponse(0, ImapConstants.QUOTA)) { + continue; + } + ImapList list = response.getListOrEmpty(2); + for (int i = 0; i < list.size(); i += 3) { + if (!list.getStringOrEmpty(i).is("voice")) { + continue; + } + return new Quota( + list.getStringOrEmpty(i + 1).getNumber(-1), + list.getStringOrEmpty(i + 2).getNumber(-1)); + } + } + } catch (IOException ioe) { + mStore.getImapHelper().handleEvent(OmtpEvents.DATA_GENERIC_IMAP_IOE); + throw ioExceptionHandler(mConnection, ioe); + } finally { + destroyResponses(); + } + return null; + } + + private void checkOpen() throws MessagingException { + if (!isOpen()) { + throw new MessagingException("Folder " + mName + " is not open."); + } + } + + private MessagingException ioExceptionHandler(ImapConnection connection, IOException ioe) { + LogUtils.d(TAG, "IO Exception detected: ", ioe); + connection.close(); + if (connection == mConnection) { + mConnection = null; // To prevent close() from returning the connection to the pool. + close(false); + } + return new MessagingException(MessagingException.IOERROR, "IO Error", ioe); + } + + public Message createMessage(String uid) { + return new ImapMessage(uid, this); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/store/ImapStore.java b/java/com/android/voicemailomtp/mail/store/ImapStore.java new file mode 100644 index 000000000..f3e0c098e --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/ImapStore.java @@ -0,0 +1,176 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.store; + +import android.content.Context; +import android.net.Network; + +import com.android.voicemailomtp.mail.MailTransport; +import com.android.voicemailomtp.mail.Message; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.mail.internet.MimeMessage; +import com.android.voicemailomtp.imap.ImapHelper; + +import java.io.IOException; +import java.io.InputStream; + +public class ImapStore { + /** + * A global suggestion to Store implementors on how much of the body + * should be returned on FetchProfile.Item.BODY_SANE requests. We'll use 125k now. + */ + public static final int FETCH_BODY_SANE_SUGGESTED_SIZE = (125 * 1024); + private final Context mContext; + private final ImapHelper mHelper; + private final String mUsername; + private final String mPassword; + private final MailTransport mTransport; + private ImapConnection mConnection; + + public static final int FLAG_NONE = 0x00; // No flags + public static final int FLAG_SSL = 0x01; // Use SSL + public static final int FLAG_TLS = 0x02; // Use TLS + public static final int FLAG_AUTHENTICATE = 0x04; // Use name/password for authentication + public static final int FLAG_TRUST_ALL = 0x08; // Trust all certificates + public static final int FLAG_OAUTH = 0x10; // Use OAuth for authentication + + /** + * Contains all the information necessary to log into an imap server + */ + public ImapStore(Context context, ImapHelper helper, String username, String password, int port, + String serverName, int flags, Network network) { + mContext = context; + mHelper = helper; + mUsername = username; + mPassword = password; + mTransport = new MailTransport(context, this.getImapHelper(), + network, serverName, port, flags); + } + + public Context getContext() { + return mContext; + } + + public ImapHelper getImapHelper() { + return mHelper; + } + + public String getUsername() { + return mUsername; + } + + public String getPassword() { + return mPassword; + } + + /** Returns a clone of the transport associated with this store. */ + MailTransport cloneTransport() { + return mTransport.clone(); + } + + /** + * Returns UIDs of Messages joined with "," as the separator. + */ + static String joinMessageUids(Message[] messages) { + StringBuilder sb = new StringBuilder(); + boolean notFirst = false; + for (Message m : messages) { + if (notFirst) { + sb.append(','); + } + sb.append(m.getUid()); + notFirst = true; + } + return sb.toString(); + } + + static class ImapMessage extends MimeMessage { + private ImapFolder mFolder; + + ImapMessage(String uid, ImapFolder folder) { + mUid = uid; + mFolder = folder; + } + + public void setSize(int size) { + mSize = size; + } + + @Override + public void parse(InputStream in) throws IOException, MessagingException { + super.parse(in); + } + + public void setFlagInternal(String flag, boolean set) throws MessagingException { + super.setFlag(flag, set); + } + + @Override + public void setFlag(String flag, boolean set) throws MessagingException { + super.setFlag(flag, set); + mFolder.setFlags(new Message[] { this }, new String[] { flag }, set); + } + } + + static class ImapException extends MessagingException { + private static final long serialVersionUID = 1L; + + private final String mStatus; + private final String mStatusMessage; + private final String mAlertText; + private final String mResponseCode; + + public ImapException(String message, String status, String statusMessage, String alertText, + String responseCode) { + super(message); + mStatus = status; + mStatusMessage = statusMessage; + mAlertText = alertText; + mResponseCode = responseCode; + } + + public String getStatus() { + return mStatus; + } + + public String getStatusMessage() { + return mStatusMessage; + } + + public String getAlertText() { + return mAlertText; + } + + public String getResponseCode() { + return mResponseCode; + } + } + + public void closeConnection() { + if (mConnection != null) { + mConnection.close(); + mConnection = null; + } + } + + public ImapConnection getConnection() { + if (mConnection == null) { + mConnection = new ImapConnection(this); + } + return mConnection; + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java b/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java new file mode 100644 index 000000000..b78f55293 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.mail.store.imap; + +import android.annotation.TargetApi; +import android.os.Build.VERSION_CODES; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.util.ArrayMap; +import android.util.Base64; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.mail.MailTransport; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.mail.store.ImapStore; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Map; + +@SuppressWarnings("AndroidApiChecker") // Map.getOrDefault() is java8 +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class DigestMd5Utils { + + private static final String TAG = "DigestMd5Utils"; + + private static final String DIGEST_CHARSET = "CHARSET"; + private static final String DIGEST_USERNAME = "username"; + private static final String DIGEST_REALM = "realm"; + private static final String DIGEST_NONCE = "nonce"; + private static final String DIGEST_NC = "nc"; + private static final String DIGEST_CNONCE = "cnonce"; + private static final String DIGEST_URI = "digest-uri"; + private static final String DIGEST_RESPONSE = "response"; + private static final String DIGEST_QOP = "qop"; + + private static final String RESPONSE_AUTH_HEADER = "rspauth="; + private static final String HEX_CHARS = "0123456789abcdef"; + + /** + * Represents the set of data we need to generate the DIGEST-MD5 response. + */ + public static class Data { + + private static final String CHARSET = "utf-8"; + + public String username; + public String password; + public String realm; + public String nonce; + public String nc; + public String cnonce; + public String digestUri; + public String qop; + + @VisibleForTesting + Data() { + // Do nothing + } + + public Data(ImapStore imapStore, MailTransport transport, Map<String, String> challenge) { + username = imapStore.getUsername(); + password = imapStore.getPassword(); + realm = challenge.getOrDefault(DIGEST_REALM, ""); + nonce = challenge.get(DIGEST_NONCE); + cnonce = createCnonce(); + nc = "00000001"; // Subsequent Authentication not supported, nounce count always 1. + qop = "auth"; // Other config not supported + digestUri = "imap/" + transport.getHost(); + } + + private static String createCnonce() { + SecureRandom generator = new SecureRandom(); + + // At least 64 bits of entropy is required + byte[] rawBytes = new byte[8]; + generator.nextBytes(rawBytes); + + return Base64.encodeToString(rawBytes, Base64.NO_WRAP); + } + + /** + * Verify the response-auth returned by the server is correct. + */ + public void verifyResponseAuth(String response) + throws MessagingException { + if (!response.startsWith(RESPONSE_AUTH_HEADER)) { + throw new MessagingException("response-auth expected"); + } + if (!response.substring(RESPONSE_AUTH_HEADER.length()) + .equals(DigestMd5Utils.getResponse(this, true))) { + throw new MessagingException("invalid response-auth return from the server."); + } + } + + public String createResponse() { + String response = getResponse(this, false); + ResponseBuilder builder = new ResponseBuilder(); + builder + .append(DIGEST_CHARSET, CHARSET) + .appendQuoted(DIGEST_USERNAME, username) + .appendQuoted(DIGEST_REALM, realm) + .appendQuoted(DIGEST_NONCE, nonce) + .append(DIGEST_NC, nc) + .appendQuoted(DIGEST_CNONCE, cnonce) + .appendQuoted(DIGEST_URI, digestUri) + .append(DIGEST_RESPONSE, response) + .append(DIGEST_QOP, qop); + return builder.toString(); + } + + private static class ResponseBuilder { + + private StringBuilder mBuilder = new StringBuilder(); + + public ResponseBuilder appendQuoted(String key, String value) { + if (mBuilder.length() != 0) { + mBuilder.append(","); + } + mBuilder.append(key).append("=\"").append(value).append("\""); + return this; + } + + public ResponseBuilder append(String key, String value) { + if (mBuilder.length() != 0) { + mBuilder.append(","); + } + mBuilder.append(key).append("=").append(value); + return this; + } + + @Override + public String toString() { + return mBuilder.toString(); + } + } + } + + /* + response-value = + toHex( getKeyDigest ( toHex(getMd5(a1)), + { nonce-value, ":" nc-value, ":", + cnonce-value, ":", qop-value, ":", toHex(getMd5(a2)) })) + * @param isResponseAuth is the response the one the server is returning us. response-auth has + * different a2 format. + */ + @VisibleForTesting + static String getResponse(Data data, boolean isResponseAuth) { + StringBuilder a1 = new StringBuilder(); + a1.append(new String( + getMd5(data.username + ":" + data.realm + ":" + data.password), + StandardCharsets.ISO_8859_1)); + a1.append(":").append(data.nonce).append(":").append(data.cnonce); + + StringBuilder a2 = new StringBuilder(); + if (!isResponseAuth) { + a2.append("AUTHENTICATE"); + } + a2.append(":").append(data.digestUri); + + return toHex(getKeyDigest( + toHex(getMd5(a1.toString())), + data.nonce + ":" + data.nc + ":" + data.cnonce + ":" + data.qop + ":" + toHex( + getMd5(a2.toString())) + )); + } + + /** + * Let getMd5(s) be the 16 octet MD5 hash [RFC 1321] of the octet string s. + */ + private static byte[] getMd5(String s) { + try { + MessageDigest digester = MessageDigest.getInstance("MD5"); + digester.update(s.getBytes(StandardCharsets.ISO_8859_1)); + return digester.digest(); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + /** + * Let getKeyDigest(k, s) be getMd5({k, ":", s}), i.e., the 16 octet hash of the string k, a colon and the + * string s. + */ + private static byte[] getKeyDigest(String k, String s) { + StringBuilder builder = new StringBuilder(k).append(":").append(s); + return getMd5(builder.toString()); + } + + /** + * Let toHex(n) be the representation of the 16 octet MD5 hash n as a string of 32 hex digits + * (with alphabetic characters always in lower case, since MD5 is case sensitive). + */ + private static String toHex(byte[] n) { + StringBuilder result = new StringBuilder(); + for (byte b : n) { + int unsignedByte = b & 0xFF; + result.append(HEX_CHARS.charAt(unsignedByte / 16)) + .append(HEX_CHARS.charAt(unsignedByte % 16)); + } + return result.toString(); + } + + public static Map<String, String> parseDigestMessage(String message) throws MessagingException { + Map<String, String> result = new DigestMessageParser(message).parse(); + if (!result.containsKey(DIGEST_NONCE)) { + throw new MessagingException("nonce missing from server DIGEST-MD5 challenge"); + } + return result; + } + + /** + * Parse the key-value pair returned by the server. + */ + private static class DigestMessageParser { + + private final String mMessage; + private int mPosition = 0; + private Map<String, String> mResult = new ArrayMap<>(); + + public DigestMessageParser(String message) { + mMessage = message; + } + + @Nullable + public Map<String, String> parse() { + try { + while (mPosition < mMessage.length()) { + parsePair(); + if (mPosition != mMessage.length()) { + expect(','); + } + } + } catch (IndexOutOfBoundsException e) { + VvmLog.e(TAG, e.toString()); + return null; + } + return mResult; + } + + private void parsePair() { + String key = parseKey(); + expect('='); + String value = parseValue(); + mResult.put(key, value); + } + + private void expect(char c) { + if (pop() != c) { + throw new IllegalStateException( + "unexpected character " + mMessage.charAt(mPosition)); + } + } + + private char pop() { + char result = peek(); + mPosition++; + return result; + } + + private char peek() { + return mMessage.charAt(mPosition); + } + + private void goToNext(char c) { + while (peek() != c) { + mPosition++; + } + } + + private String parseKey() { + int start = mPosition; + goToNext('='); + return mMessage.substring(start, mPosition); + } + + private String parseValue() { + if (peek() == '"') { + return parseQuotedValue(); + } else { + return parseUnquotedValue(); + } + } + + private String parseQuotedValue() { + expect('"'); + StringBuilder result = new StringBuilder(); + while (true) { + char c = pop(); + if (c == '\\') { + result.append(pop()); + } else if (c == '"') { + break; + } else { + result.append(c); + } + } + return result.toString(); + } + + private String parseUnquotedValue() { + StringBuilder result = new StringBuilder(); + while (true) { + char c = pop(); + if (c == '\\') { + result.append(pop()); + } else if (c == ',') { + mPosition--; + break; + } else { + result.append(c); + } + + if (mPosition == mMessage.length()) { + break; + } + } + return result.toString(); + } + } +} diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java b/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java new file mode 100644 index 000000000..d8e75752f --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.store.imap; + +import com.android.voicemailomtp.mail.store.ImapStore; + +import java.util.Locale; + +public final class ImapConstants { + private ImapConstants() {} + + public static final String FETCH_FIELD_BODY_PEEK_BARE = "BODY.PEEK"; + public static final String FETCH_FIELD_BODY_PEEK = FETCH_FIELD_BODY_PEEK_BARE + "[]"; + public static final String FETCH_FIELD_BODY_PEEK_SANE = String.format( + Locale.US, "BODY.PEEK[]<0.%d>", ImapStore.FETCH_BODY_SANE_SUGGESTED_SIZE); + public static final String FETCH_FIELD_HEADERS = + "BODY.PEEK[HEADER.FIELDS (date subject from content-type to cc message-id)]"; + + public static final String ALERT = "ALERT"; + public static final String APPEND = "APPEND"; + public static final String AUTHENTICATE = "AUTHENTICATE"; + public static final String BAD = "BAD"; + public static final String BADCHARSET = "BADCHARSET"; + public static final String BODY = "BODY"; + public static final String BODY_BRACKET_HEADER = "BODY[HEADER"; + public static final String BODYSTRUCTURE = "BODYSTRUCTURE"; + public static final String BYE = "BYE"; + public static final String CAPABILITY = "CAPABILITY"; + public static final String CHECK = "CHECK"; + public static final String CLOSE = "CLOSE"; + public static final String COPY = "COPY"; + public static final String COPYUID = "COPYUID"; + public static final String CREATE = "CREATE"; + public static final String DELETE = "DELETE"; + public static final String EXAMINE = "EXAMINE"; + public static final String EXISTS = "EXISTS"; + public static final String EXPUNGE = "EXPUNGE"; + public static final String FETCH = "FETCH"; + public static final String FLAG_ANSWERED = "\\ANSWERED"; + public static final String FLAG_DELETED = "\\DELETED"; + public static final String FLAG_FLAGGED = "\\FLAGGED"; + public static final String FLAG_NO_SELECT = "\\NOSELECT"; + public static final String FLAG_SEEN = "\\SEEN"; + public static final String FLAGS = "FLAGS"; + public static final String FLAGS_SILENT = "FLAGS.SILENT"; + public static final String ID = "ID"; + public static final String INBOX = "INBOX"; + public static final String INTERNALDATE = "INTERNALDATE"; + public static final String LIST = "LIST"; + public static final String LOGIN = "LOGIN"; + public static final String LOGOUT = "LOGOUT"; + public static final String LSUB = "LSUB"; + public static final String NAMESPACE = "NAMESPACE"; + public static final String NO = "NO"; + public static final String NOOP = "NOOP"; + public static final String OK = "OK"; + public static final String PARSE = "PARSE"; + public static final String PERMANENTFLAGS = "PERMANENTFLAGS"; + public static final String PREAUTH = "PREAUTH"; + public static final String READ_ONLY = "READ-ONLY"; + public static final String READ_WRITE = "READ-WRITE"; + public static final String RENAME = "RENAME"; + public static final String RFC822_SIZE = "RFC822.SIZE"; + public static final String SEARCH = "SEARCH"; + public static final String SELECT = "SELECT"; + public static final String STARTTLS = "STARTTLS"; + public static final String STATUS = "STATUS"; + public static final String STORE = "STORE"; + public static final String SUBSCRIBE = "SUBSCRIBE"; + public static final String TEXT = "TEXT"; + public static final String TRYCREATE = "TRYCREATE"; + public static final String UID = "UID"; + public static final String UID_COPY = "UID COPY"; + public static final String UID_FETCH = "UID FETCH"; + public static final String UID_SEARCH = "UID SEARCH"; + public static final String UID_STORE = "UID STORE"; + public static final String UIDNEXT = "UIDNEXT"; + public static final String UIDPLUS = "UIDPLUS"; + public static final String UIDVALIDITY = "UIDVALIDITY"; + public static final String UNSEEN = "UNSEEN"; + public static final String UNSUBSCRIBE = "UNSUBSCRIBE"; + public static final String XOAUTH2 = "XOAUTH2"; + public static final String APPENDUID = "APPENDUID"; + public static final String NIL = "NIL"; + + /** + * NO responses + */ + public static final String NO_COMMAND_NOT_ALLOWED = "command not allowed"; + public static final String NO_RESERVATION_FAILED = "reservation failed"; + public static final String NO_APPLICATION_ERROR = "application error"; + public static final String NO_INVALID_PARAMETER = "invalid parameter"; + public static final String NO_INVALID_COMMAND = "invalid command"; + public static final String NO_UNKNOWN_COMMAND = "unknown command"; + // AUTHENTICATE + // The subscriber can not be located in the system. + public static final String NO_UNKNOWN_USER = "unknown user"; + // The Client Type or Protocol Version is unknown. + public static final String NO_UNKNOWN_CLIENT = "unknown client"; + // The password received from the client does not match the password defined in the subscriber's profile. + public static final String NO_INVALID_PASSWORD = "invalid password"; + // The subscriber's mailbox has not yet been initialised via the TUI + public static final String NO_MAILBOX_NOT_INITIALIZED = "mailbox not initialized"; + // The subscriber has not been provisioned for the VVM service. + public static final String NO_SERVICE_IS_NOT_PROVISIONED = + "service is not provisioned"; + // The subscriber is provisioned for the VVM service but the VVM service is currently not active + public static final String NO_SERVICE_IS_NOT_ACTIVATED = "service is not activated"; + // The Voice Mail Blocked flag in the subscriber's profile is set to YES. + public static final String NO_USER_IS_BLOCKED = "user is blocked"; + + /** + * extensions + */ + public static final String GETQUOTA = "GETQUOTA"; + public static final String GETQUOTAROOT = "GETQUOTAROOT"; + public static final String QUOTAROOT = "QUOTAROOT"; + public static final String QUOTA = "QUOTA"; + + /** + * capabilities + */ + public static final String CAPABILITY_AUTH_DIGEST_MD5 = "AUTH=DIGEST-MD5"; + public static final String CAPABILITY_STARTTLS = "STARTTLS"; + + /** + * authentication + */ + public static final String AUTH_DIGEST_MD5 = "DIGEST-MD5"; +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java b/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java new file mode 100644 index 000000000..9f272e31c --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/imap/ImapElement.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.store.imap; + +/** + * Class representing "element"s in IMAP responses. + * + * <p>Class hierarchy: + * <pre> + * ImapElement + * | + * |-- ImapElement.NONE (for 'index out of range') + * | + * |-- ImapList (isList() == true) + * | | + * | |-- ImapList.EMPTY + * | | + * | --- ImapResponse + * | + * --- ImapString (isString() == true) + * | + * |-- ImapString.EMPTY + * | + * |-- ImapSimpleString + * | + * |-- ImapMemoryLiteral + * | + * --- ImapTempFileLiteral + * </pre> + */ +public abstract class ImapElement { + /** + * An element that is returned by {@link ImapList#getElementOrNone} to indicate an index + * is out of range. + */ + public static final ImapElement NONE = new ImapElement() { + @Override public void destroy() { + // Don't call super.destroy(). + // It's a shared object. We don't want the mDestroyed to be set on this. + } + + @Override public boolean isList() { + return false; + } + + @Override public boolean isString() { + return false; + } + + @Override public String toString() { + return "[NO ELEMENT]"; + } + + @Override + public boolean equalsForTest(ImapElement that) { + return super.equalsForTest(that); + } + }; + + private boolean mDestroyed = false; + + public abstract boolean isList(); + + public abstract boolean isString(); + + protected boolean isDestroyed() { + return mDestroyed; + } + + /** + * Clean up the resources used by the instance. + * It's for removing a temp file used by {@link ImapTempFileLiteral}. + */ + public void destroy() { + mDestroyed = true; + } + + /** + * Throws {@link RuntimeException} if it's already destroyed. + */ + protected final void checkNotDestroyed() { + if (mDestroyed) { + throw new RuntimeException("Already destroyed"); + } + } + + /** + * Return a string that represents this object; it's purely for the debug purpose. Don't + * mistake it for {@link ImapString#getString}. + * + * Abstract to force subclasses to implement it. + */ + @Override + public abstract String toString(); + + /** + * The equals implementation that is intended to be used only for unit testing. + * (Because it may be heavy and has a special sense of "equal" for testing.) + */ + public boolean equalsForTest(ImapElement that) { + if (that == null) { + return false; + } + return this.getClass() == that.getClass(); // Has to be the same class. + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapList.java b/java/com/android/voicemailomtp/mail/store/imap/ImapList.java new file mode 100644 index 000000000..970423cbd --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/imap/ImapList.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.store.imap; + +import java.util.ArrayList; + +/** + * Class represents an IMAP list. + */ +public class ImapList extends ImapElement { + /** + * {@link ImapList} representing an empty list. + */ + public static final ImapList EMPTY = new ImapList() { + @Override public void destroy() { + // Don't call super.destroy(). + // It's a shared object. We don't want the mDestroyed to be set on this. + } + + @Override void add(ImapElement e) { + throw new RuntimeException(); + } + }; + + private ArrayList<ImapElement> mList = new ArrayList<ImapElement>(); + + /* package */ void add(ImapElement e) { + if (e == null) { + throw new RuntimeException("Can't add null"); + } + mList.add(e); + } + + @Override + public final boolean isString() { + return false; + } + + @Override + public final boolean isList() { + return true; + } + + public final int size() { + return mList.size(); + } + + public final boolean isEmpty() { + return size() == 0; + } + + /** + * Return true if the element at {@code index} exists, is string, and equals to {@code s}. + * (case insensitive) + */ + public final boolean is(int index, String s) { + return is(index, s, false); + } + + /** + * Same as {@link #is(int, String)}, but does the prefix match if {@code prefixMatch}. + */ + public final boolean is(int index, String s, boolean prefixMatch) { + if (!prefixMatch) { + return getStringOrEmpty(index).is(s); + } else { + return getStringOrEmpty(index).startsWith(s); + } + } + + /** + * Return the element at {@code index}. + * If {@code index} is out of range, returns {@link ImapElement#NONE}. + */ + public final ImapElement getElementOrNone(int index) { + return (index >= mList.size()) ? ImapElement.NONE : mList.get(index); + } + + /** + * Return the element at {@code index} if it's a list. + * If {@code index} is out of range or not a list, returns {@link ImapList#EMPTY}. + */ + public final ImapList getListOrEmpty(int index) { + ImapElement el = getElementOrNone(index); + return el.isList() ? (ImapList) el : EMPTY; + } + + /** + * Return the element at {@code index} if it's a string. + * If {@code index} is out of range or not a string, returns {@link ImapString#EMPTY}. + */ + public final ImapString getStringOrEmpty(int index) { + ImapElement el = getElementOrNone(index); + return el.isString() ? (ImapString) el : ImapString.EMPTY; + } + + /** + * Return an element keyed by {@code key}. Return null if not found. {@code key} has to be + * at an even index. + */ + /* package */ final ImapElement getKeyedElementOrNull(String key, boolean prefixMatch) { + for (int i = 1; i < size(); i += 2) { + if (is(i-1, key, prefixMatch)) { + return mList.get(i); + } + } + return null; + } + + /** + * Return an {@link ImapList} keyed by {@code key}. + * Return {@link ImapList#EMPTY} if not found. + */ + public final ImapList getKeyedListOrEmpty(String key) { + return getKeyedListOrEmpty(key, false); + } + + /** + * Return an {@link ImapList} keyed by {@code key}. + * Return {@link ImapList#EMPTY} if not found. + */ + public final ImapList getKeyedListOrEmpty(String key, boolean prefixMatch) { + ImapElement e = getKeyedElementOrNull(key, prefixMatch); + return (e != null) ? ((ImapList) e) : ImapList.EMPTY; + } + + /** + * Return an {@link ImapString} keyed by {@code key}. + * Return {@link ImapString#EMPTY} if not found. + */ + public final ImapString getKeyedStringOrEmpty(String key) { + return getKeyedStringOrEmpty(key, false); + } + + /** + * Return an {@link ImapString} keyed by {@code key}. + * Return {@link ImapString#EMPTY} if not found. + */ + public final ImapString getKeyedStringOrEmpty(String key, boolean prefixMatch) { + ImapElement e = getKeyedElementOrNull(key, prefixMatch); + return (e != null) ? ((ImapString) e) : ImapString.EMPTY; + } + + /** + * Return true if it contains {@code s}. + */ + public final boolean contains(String s) { + for (int i = 0; i < size(); i++) { + if (getStringOrEmpty(i).is(s)) { + return true; + } + } + return false; + } + + @Override + public void destroy() { + if (mList != null) { + for (ImapElement e : mList) { + e.destroy(); + } + mList = null; + } + super.destroy(); + } + + @Override + public String toString() { + return mList.toString(); + } + + /** + * Return the text representations of the contents concatenated with ",". + */ + public final String flatten() { + return flatten(new StringBuilder()).toString(); + } + + /** + * Returns text representations (i.e. getString()) of contents joined together with + * "," as the separator. + * + * Only used for building the capability string passed to vendor policies. + * + * We can't use toString(), because it's for debugging (meaning the format may change any time), + * and it won't expand literals. + */ + private final StringBuilder flatten(StringBuilder sb) { + sb.append('['); + for (int i = 0; i < mList.size(); i++) { + if (i > 0) { + sb.append(','); + } + final ImapElement e = getElementOrNone(i); + if (e.isList()) { + getListOrEmpty(i).flatten(sb); + } else if (e.isString()) { + sb.append(getStringOrEmpty(i).getString()); + } + } + sb.append(']'); + return sb; + } + + @Override + public boolean equalsForTest(ImapElement that) { + if (!super.equalsForTest(that)) { + return false; + } + ImapList thatList = (ImapList) that; + if (size() != thatList.size()) { + return false; + } + for (int i = 0; i < size(); i++) { + if (!mList.get(i).equalsForTest(thatList.getElementOrNone(i))) { + return false; + } + } + return true; + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java b/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java new file mode 100644 index 000000000..ad60ca7a4 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2010 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.android.voicemailomtp.mail.store.imap; + +import com.android.voicemailomtp.mail.FixedLengthInputStream; +import com.android.voicemailomtp.VvmLog; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +/** + * Subclass of {@link ImapString} used for literals backed by an in-memory byte array. + */ +public class ImapMemoryLiteral extends ImapString { + private final String TAG = "ImapMemoryLiteral"; + private byte[] mData; + + /* package */ ImapMemoryLiteral(FixedLengthInputStream in) throws IOException { + // We could use ByteArrayOutputStream and IOUtils.copy, but it'd perform an unnecessary + // copy.... + mData = new byte[in.getLength()]; + int pos = 0; + while (pos < mData.length) { + int read = in.read(mData, pos, mData.length - pos); + if (read < 0) { + break; + } + pos += read; + } + if (pos != mData.length) { + VvmLog.w(TAG, "length mismatch"); + } + } + + @Override + public void destroy() { + mData = null; + super.destroy(); + } + + @Override + public String getString() { + try { + return new String(mData, "US-ASCII"); + } catch (UnsupportedEncodingException e) { + VvmLog.e(TAG, "Unsupported encoding: ", e); + } + return null; + } + + @Override + public InputStream getAsStream() { + return new ByteArrayInputStream(mData); + } + + @Override + public String toString() { + return String.format("{%d byte literal(memory)}", mData.length); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java b/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java new file mode 100644 index 000000000..412f16d8a --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.store.imap; + +/** + * Class represents an IMAP response. + */ +public class ImapResponse extends ImapList { + private final String mTag; + private final boolean mIsContinuationRequest; + + /* package */ ImapResponse(String tag, boolean isContinuationRequest) { + mTag = tag; + mIsContinuationRequest = isContinuationRequest; + } + + /* package */ static boolean isStatusResponse(String symbol) { + return ImapConstants.OK.equalsIgnoreCase(symbol) + || ImapConstants.NO.equalsIgnoreCase(symbol) + || ImapConstants.BAD.equalsIgnoreCase(symbol) + || ImapConstants.PREAUTH.equalsIgnoreCase(symbol) + || ImapConstants.BYE.equalsIgnoreCase(symbol); + } + + /** + * @return whether it's a tagged response. + */ + public boolean isTagged() { + return mTag != null; + } + + /** + * @return whether it's a continuation request. + */ + public boolean isContinuationRequest() { + return mIsContinuationRequest; + } + + public boolean isStatusResponse() { + return isStatusResponse(getStringOrEmpty(0).getString()); + } + + /** + * @return whether it's an OK response. + */ + public boolean isOk() { + return is(0, ImapConstants.OK); + } + + /** + * @return whether it's an BAD response. + */ + public boolean isBad() { + return is(0, ImapConstants.BAD); + } + + /** + * @return whether it's an NO response. + */ + public boolean isNo() { + return is(0, ImapConstants.NO); + } + + /** + * @return whether it's an {@code responseType} data response. (i.e. not tagged). + * @param index where {@code responseType} should appear. e.g. 1 for "FETCH" + * @param responseType e.g. "FETCH" + */ + public final boolean isDataResponse(int index, String responseType) { + return !isTagged() && getStringOrEmpty(index).is(responseType); + } + + /** + * @return Response code (RFC 3501 7.1) if it's a status response. + * + * e.g. "ALERT" for "* OK [ALERT] System shutdown in 10 minutes" + */ + public ImapString getResponseCodeOrEmpty() { + if (!isStatusResponse()) { + return ImapString.EMPTY; // Not a status response. + } + return getListOrEmpty(1).getStringOrEmpty(0); + } + + /** + * @return Alert message it it has ALERT response code. + * + * e.g. "System shutdown in 10 minutes" for "* OK [ALERT] System shutdown in 10 minutes" + */ + public ImapString getAlertTextOrEmpty() { + if (!getResponseCodeOrEmpty().is(ImapConstants.ALERT)) { + return ImapString.EMPTY; // Not an ALERT + } + // The 3rd element contains all the rest of line. + return getStringOrEmpty(2); + } + + /** + * @return Response text in a status response. + */ + public ImapString getStatusResponseTextOrEmpty() { + if (!isStatusResponse()) { + return ImapString.EMPTY; + } + return getStringOrEmpty(getElementOrNone(1).isList() ? 2 : 1); + } + + public ImapString getStatusOrEmpty() { + if (!isStatusResponse()) { + return ImapString.EMPTY; + } + return getStringOrEmpty(0); + } + + @Override + public String toString() { + String tag = mTag; + if (isContinuationRequest()) { + tag = "+"; + } + return "#" + tag + "# " + super.toString(); + } + + @Override + public boolean equalsForTest(ImapElement that) { + if (!super.equalsForTest(that)) { + return false; + } + final ImapResponse thatResponse = (ImapResponse) that; + if (mTag == null) { + if (thatResponse.mTag != null) { + return false; + } + } else { + if (!mTag.equals(thatResponse.mTag)) { + return false; + } + } + if (mIsContinuationRequest != thatResponse.mIsContinuationRequest) { + return false; + } + return true; + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java b/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java new file mode 100644 index 000000000..692596f14 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java @@ -0,0 +1,432 @@ +/* + * Copyright (C) 2010 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.android.voicemailomtp.mail.store.imap; + +import android.text.TextUtils; +import android.util.Log; + +import com.android.voicemailomtp.mail.FixedLengthInputStream; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.mail.PeekableInputStream; +import com.android.voicemailomtp.VvmLog; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; + +/** + * IMAP response parser. + */ +public class ImapResponseParser { + private static final String TAG = "ImapResponseParser"; + + /** + * Literal larger than this will be stored in temp file. + */ + public static final int LITERAL_KEEP_IN_MEMORY_THRESHOLD = 2 * 1024 * 1024; + + /** Input stream */ + private final PeekableInputStream mIn; + + private final int mLiteralKeepInMemoryThreshold; + + /** StringBuilder used by readUntil() */ + private final StringBuilder mBufferReadUntil = new StringBuilder(); + + /** StringBuilder used by parseBareString() */ + private final StringBuilder mParseBareString = new StringBuilder(); + + /** + * We store all {@link ImapResponse} in it. {@link #destroyResponses()} must be called from + * time to time to destroy them and clear it. + */ + private final ArrayList<ImapResponse> mResponsesToDestroy = new ArrayList<ImapResponse>(); + + /** + * Exception thrown when we receive BYE. It derives from IOException, so it'll be treated + * in the same way EOF does. + */ + public static class ByeException extends IOException { + public static final String MESSAGE = "Received BYE"; + public ByeException() { + super(MESSAGE); + } + } + + /** + * Public constructor for normal use. + */ + public ImapResponseParser(InputStream in) { + this(in, LITERAL_KEEP_IN_MEMORY_THRESHOLD); + } + + /** + * Constructor for testing to override the literal size threshold. + */ + /* package for test */ ImapResponseParser(InputStream in, int literalKeepInMemoryThreshold) { + mIn = new PeekableInputStream(in); + mLiteralKeepInMemoryThreshold = literalKeepInMemoryThreshold; + } + + private static IOException newEOSException() { + final String message = "End of stream reached"; + VvmLog.d(TAG, message); + return new IOException(message); + } + + /** + * Peek next one byte. + * + * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, + * we shouldn't see EOF during parsing. + */ + private int peek() throws IOException { + final int next = mIn.peek(); + if (next == -1) { + throw newEOSException(); + } + return next; + } + + /** + * Read and return one byte from {@link #mIn}, and put it in {@link #mDiscourseLogger}. + * + * Throws IOException() if reaches EOF. As long as logical response lines end with \r\n, + * we shouldn't see EOF during parsing. + */ + private int readByte() throws IOException { + int next = mIn.read(); + if (next == -1) { + throw newEOSException(); + } + return next; + } + + /** + * Destroy all the {@link ImapResponse}s stored in the internal storage and clear it. + * + * @see #readResponse() + */ + public void destroyResponses() { + for (ImapResponse r : mResponsesToDestroy) { + r.destroy(); + } + mResponsesToDestroy.clear(); + } + + /** + * Reads the next response available on the stream and returns an + * {@link ImapResponse} object that represents it. + * + * <p>When this method successfully returns an {@link ImapResponse}, the {@link ImapResponse} + * is stored in the internal storage. When the {@link ImapResponse} is no longer used + * {@link #destroyResponses} should be called to destroy all the responses in the array. + * + * @param byeExpected is a untagged BYE response expected? If not proper cleanup will be done + * and {@link ByeException} will be thrown. + * @return the parsed {@link ImapResponse} object. + * @exception ByeException when detects BYE and <code>byeExpected</code> is false. + */ + public ImapResponse readResponse(boolean byeExpected) throws IOException, MessagingException { + ImapResponse response = null; + try { + response = parseResponse(); + } catch (RuntimeException e) { + // Parser crash -- log network activities. + onParseError(e); + throw e; + } catch (IOException e) { + // Network error, or received an unexpected char. + onParseError(e); + throw e; + } + + // Handle this outside of try-catch. We don't have to dump protocol log when getting BYE. + if (!byeExpected && response.is(0, ImapConstants.BYE)) { + Log.w(TAG, ByeException.MESSAGE); + response.destroy(); + throw new ByeException(); + } + mResponsesToDestroy.add(response); + return response; + } + + private void onParseError(Exception e) { + // Read a few more bytes, so that the log will contain some more context, even if the parser + // crashes in the middle of a response. + // This also makes sure the byte in question will be logged, no matter where it crashes. + // e.g. when parseAtom() peeks and finds at an unexpected char, it throws an exception + // before actually reading it. + // However, we don't want to read too much, because then it may get into an email message. + try { + for (int i = 0; i < 4; i++) { + int b = readByte(); + if (b == -1 || b == '\n') { + break; + } + } + } catch (IOException ignore) { + } + VvmLog.w(TAG, "Exception detected: " + e.getMessage()); + } + + /** + * Read next byte from stream and throw it away. If the byte is different from {@code expected} + * throw {@link MessagingException}. + */ + /* package for test */ void expect(char expected) throws IOException { + final int next = readByte(); + if (expected != next) { + throw new IOException(String.format("Expected %04x (%c) but got %04x (%c)", + (int) expected, expected, next, (char) next)); + } + } + + /** + * Read bytes until we find {@code end}, and return all as string. + * The {@code end} will be read (rather than peeked) and won't be included in the result. + */ + /* package for test */ String readUntil(char end) throws IOException { + mBufferReadUntil.setLength(0); + for (;;) { + final int ch = readByte(); + if (ch != end) { + mBufferReadUntil.append((char) ch); + } else { + return mBufferReadUntil.toString(); + } + } + } + + /** + * Read all bytes until \r\n. + */ + /* package */ String readUntilEol() throws IOException { + String ret = readUntil('\r'); + expect('\n'); // TODO Should this really be error? + return ret; + } + + /** + * Parse and return the response line. + */ + private ImapResponse parseResponse() throws IOException, MessagingException { + // We need to destroy the response if we get an exception. + // So, we first store the response that's being built in responseToDestroy, until it's + // completely built, at which point we copy it into responseToReturn and null out + // responseToDestroyt. + // If responseToDestroy is not null in finally, we destroy it because that means + // we got an exception somewhere. + ImapResponse responseToDestroy = null; + final ImapResponse responseToReturn; + + try { + final int ch = peek(); + if (ch == '+') { // Continuation request + readByte(); // skip + + expect(' '); + responseToDestroy = new ImapResponse(null, true); + + // If it's continuation request, we don't really care what's in it. + responseToDestroy.add(new ImapSimpleString(readUntilEol())); + + // Response has successfully been built. Let's return it. + responseToReturn = responseToDestroy; + responseToDestroy = null; + } else { + // Status response or response data + final String tag; + if (ch == '*') { + tag = null; + readByte(); // skip * + expect(' '); + } else { + tag = readUntil(' '); + } + responseToDestroy = new ImapResponse(tag, false); + + final ImapString firstString = parseBareString(); + responseToDestroy.add(firstString); + + // parseBareString won't eat a space after the string, so we need to skip it, + // if exists. + // If the next char is not ' ', it should be EOL. + if (peek() == ' ') { + readByte(); // skip ' ' + + if (responseToDestroy.isStatusResponse()) { // It's a status response + + // Is there a response code? + final int next = peek(); + if (next == '[') { + responseToDestroy.add(parseList('[', ']')); + if (peek() == ' ') { // Skip following space + readByte(); + } + } + + String rest = readUntilEol(); + if (!TextUtils.isEmpty(rest)) { + // The rest is free-form text. + responseToDestroy.add(new ImapSimpleString(rest)); + } + } else { // It's a response data. + parseElements(responseToDestroy, '\0'); + } + } else { + expect('\r'); + expect('\n'); + } + + // Response has successfully been built. Let's return it. + responseToReturn = responseToDestroy; + responseToDestroy = null; + } + } finally { + if (responseToDestroy != null) { + // We get an exception. + responseToDestroy.destroy(); + } + } + + return responseToReturn; + } + + private ImapElement parseElement() throws IOException, MessagingException { + final int next = peek(); + switch (next) { + case '(': + return parseList('(', ')'); + case '[': + return parseList('[', ']'); + case '"': + readByte(); // Skip " + return new ImapSimpleString(readUntil('"')); + case '{': + return parseLiteral(); + case '\r': // CR + readByte(); // Consume \r + expect('\n'); // Should be followed by LF. + return null; + case '\n': // LF // There shouldn't be a bare LF, but just in case. + readByte(); // Consume \n + return null; + default: + return parseBareString(); + } + } + + /** + * Parses an atom. + * + * Special case: If an atom contains '[', everything until the next ']' will be considered + * a part of the atom. + * (e.g. "BODY[HEADER.FIELDS ("DATE" ...)]" will become a single ImapString) + * + * If the value is "NIL", returns an empty string. + */ + private ImapString parseBareString() throws IOException, MessagingException { + mParseBareString.setLength(0); + for (;;) { + final int ch = peek(); + + // TODO Can we clean this up? (This condition is from the old parser.) + if (ch == '(' || ch == ')' || ch == '{' || ch == ' ' || + // ']' is not part of atom (it's in resp-specials) + ch == ']' || + // docs claim that flags are \ atom but atom isn't supposed to + // contain + // * and some flags contain * + // ch == '%' || ch == '*' || + ch == '%' || + // TODO probably should not allow \ and should recognize + // it as a flag instead + // ch == '"' || ch == '\' || + ch == '"' || (0x00 <= ch && ch <= 0x1f) || ch == 0x7f) { + if (mParseBareString.length() == 0) { + throw new MessagingException("Expected string, none found."); + } + String s = mParseBareString.toString(); + + // NIL will be always converted into the empty string. + if (ImapConstants.NIL.equalsIgnoreCase(s)) { + return ImapString.EMPTY; + } + return new ImapSimpleString(s); + } else if (ch == '[') { + // Eat all until next ']' + mParseBareString.append((char) readByte()); + mParseBareString.append(readUntil(']')); + mParseBareString.append(']'); // readUntil won't include the end char. + } else { + mParseBareString.append((char) readByte()); + } + } + } + + private void parseElements(ImapList list, char end) + throws IOException, MessagingException { + for (;;) { + for (;;) { + final int next = peek(); + if (next == end) { + return; + } + if (next != ' ') { + break; + } + // Skip space + readByte(); + } + final ImapElement el = parseElement(); + if (el == null) { // EOL + return; + } + list.add(el); + } + } + + private ImapList parseList(char opening, char closing) + throws IOException, MessagingException { + expect(opening); + final ImapList list = new ImapList(); + parseElements(list, closing); + expect(closing); + return list; + } + + private ImapString parseLiteral() throws IOException, MessagingException { + expect('{'); + final int size; + try { + size = Integer.parseInt(readUntil('}')); + } catch (NumberFormatException nfe) { + throw new MessagingException("Invalid length in literal"); + } + if (size < 0) { + throw new MessagingException("Invalid negative length in literal"); + } + expect('\r'); + expect('\n'); + FixedLengthInputStream in = new FixedLengthInputStream(mIn, size); + if (size > mLiteralKeepInMemoryThreshold) { + return new ImapTempFileLiteral(in); + } else { + return new ImapMemoryLiteral(in); + } + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java b/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java new file mode 100644 index 000000000..22d8141a0 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.store.imap; + +import com.android.voicemailomtp.VvmLog; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +/** + * Subclass of {@link ImapString} used for non literals. + */ +public class ImapSimpleString extends ImapString { + private final String TAG = "ImapSimpleString"; + private String mString; + + /* package */ ImapSimpleString(String string) { + mString = (string != null) ? string : ""; + } + + @Override + public void destroy() { + mString = null; + super.destroy(); + } + + @Override + public String getString() { + return mString; + } + + @Override + public InputStream getAsStream() { + try { + return new ByteArrayInputStream(mString.getBytes("US-ASCII")); + } catch (UnsupportedEncodingException e) { + VvmLog.e(TAG, "Unsupported encoding: ", e); + } + return null; + } + + @Override + public String toString() { + // Purposefully not return just mString, in order to prevent using it instead of getString. + return "\"" + mString + "\""; + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapString.java b/java/com/android/voicemailomtp/mail/store/imap/ImapString.java new file mode 100644 index 000000000..83efb6479 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/imap/ImapString.java @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.store.imap; + +import com.android.voicemailomtp.VvmLog; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * Class represents an IMAP "element" that is not a list. + * + * An atom, quoted string, literal, are all represented by this. Values like OK, STATUS are too. + * Also, this class class may contain more arbitrary value like "BODY[HEADER.FIELDS ("DATE")]". + * See {@link ImapResponseParser}. + */ +public abstract class ImapString extends ImapElement { + private static final byte[] EMPTY_BYTES = new byte[0]; + + public static final ImapString EMPTY = new ImapString() { + @Override public void destroy() { + // Don't call super.destroy(). + // It's a shared object. We don't want the mDestroyed to be set on this. + } + + @Override public String getString() { + return ""; + } + + @Override public InputStream getAsStream() { + return new ByteArrayInputStream(EMPTY_BYTES); + } + + @Override public String toString() { + return ""; + } + }; + + // This is used only for parsing IMAP's FETCH ENVELOPE command, in which + // en_US-like date format is used like "01-Jan-2009 11:20:39 -0800", so this should be + // handled by Locale.US + private final static SimpleDateFormat DATE_TIME_FORMAT = + new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss Z", Locale.US); + + private boolean mIsInteger; + private int mParsedInteger; + private Date mParsedDate; + + @Override + public final boolean isList() { + return false; + } + + @Override + public final boolean isString() { + return true; + } + + /** + * @return true if and only if the length of the string is larger than 0. + * + * Note: IMAP NIL is considered an empty string. See {@link ImapResponseParser + * #parseBareString}. + * On the other hand, a quoted/literal string with value NIL (i.e. "NIL" and {3}\r\nNIL) is + * treated literally. + */ + public final boolean isEmpty() { + return getString().length() == 0; + } + + public abstract String getString(); + + public abstract InputStream getAsStream(); + + /** + * @return whether it can be parsed as a number. + */ + public final boolean isNumber() { + if (mIsInteger) { + return true; + } + try { + mParsedInteger = Integer.parseInt(getString()); + mIsInteger = true; + return true; + } catch (NumberFormatException e) { + return false; + } + } + + /** + * @return value parsed as a number, or 0 if the string is not a number. + */ + public final int getNumberOrZero() { + return getNumber(0); + } + + /** + * @return value parsed as a number, or {@code defaultValue} if the string is not a number. + */ + public final int getNumber(int defaultValue) { + if (!isNumber()) { + return defaultValue; + } + return mParsedInteger; + } + + /** + * @return whether it can be parsed as a date using {@link #DATE_TIME_FORMAT}. + */ + public final boolean isDate() { + if (mParsedDate != null) { + return true; + } + if (isEmpty()) { + return false; + } + try { + mParsedDate = DATE_TIME_FORMAT.parse(getString()); + return true; + } catch (ParseException e) { + VvmLog.w("ImapString", getString() + " can't be parsed as a date."); + return false; + } + } + + /** + * @return value it can be parsed as a {@link Date}, or null otherwise. + */ + public final Date getDateOrNull() { + if (!isDate()) { + return null; + } + return mParsedDate; + } + + /** + * @return whether the value case-insensitively equals to {@code s}. + */ + public final boolean is(String s) { + if (s == null) { + return false; + } + return getString().equalsIgnoreCase(s); + } + + + /** + * @return whether the value case-insensitively starts with {@code s}. + */ + public final boolean startsWith(String prefix) { + if (prefix == null) { + return false; + } + final String me = this.getString(); + if (me.length() < prefix.length()) { + return false; + } + return me.substring(0, prefix.length()).equalsIgnoreCase(prefix); + } + + // To force subclasses to implement it. + @Override + public abstract String toString(); + + @Override + public final boolean equalsForTest(ImapElement that) { + if (!super.equalsForTest(that)) { + return false; + } + ImapString thatString = (ImapString) that; + return getString().equals(thatString.getString()); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java b/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java new file mode 100644 index 000000000..efe5c3848 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.store.imap; + +import com.android.voicemailomtp.mail.FixedLengthInputStream; +import com.android.voicemailomtp.mail.TempDirectory; +import com.android.voicemailomtp.mail.utils.Utility; +import com.android.voicemailomtp.mail.utils.LogUtils; + +import org.apache.commons.io.IOUtils; + +import java.io.ByteArrayInputStream; +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.io.OutputStream; + +/** + * Subclass of {@link ImapString} used for literals backed by a temp file. + */ +public class ImapTempFileLiteral extends ImapString { + private final String TAG = "ImapTempFileLiteral"; + + /* package for test */ final File mFile; + + /** Size is purely for toString() */ + private final int mSize; + + /* package */ ImapTempFileLiteral(FixedLengthInputStream stream) throws IOException { + mSize = stream.getLength(); + mFile = File.createTempFile("imap", ".tmp", TempDirectory.getTempDirectory()); + + // Unfortunately, we can't really use deleteOnExit(), because temp filenames are random + // so it'd simply cause a memory leak. + // deleteOnExit() simply adds filenames to a static list and the list will never shrink. + // mFile.deleteOnExit(); + OutputStream out = new FileOutputStream(mFile); + IOUtils.copy(stream, out); + out.close(); + } + + /** + * Make sure we delete the temp file. + * + * We should always be calling {@link ImapResponse#destroy()}, but it's here as a last resort. + */ + @Override + protected void finalize() throws Throwable { + try { + destroy(); + } finally { + super.finalize(); + } + } + + @Override + public InputStream getAsStream() { + checkNotDestroyed(); + try { + return new FileInputStream(mFile); + } catch (FileNotFoundException e) { + // It's probably possible if we're low on storage and the system clears the cache dir. + LogUtils.w(TAG, "ImapTempFileLiteral: Temp file not found"); + + // Return 0 byte stream as a dummy... + return new ByteArrayInputStream(new byte[0]); + } + } + + @Override + public String getString() { + checkNotDestroyed(); + try { + byte[] bytes = IOUtils.toByteArray(getAsStream()); + // Prevent crash from OOM; we've seen this, but only rarely and not reproducibly + if (bytes.length > ImapResponseParser.LITERAL_KEEP_IN_MEMORY_THRESHOLD) { + throw new IOException(); + } + return Utility.fromAscii(bytes); + } catch (IOException e) { + LogUtils.w(TAG, "ImapTempFileLiteral: Error while reading temp file", e); + return ""; + } + } + + @Override + public void destroy() { + try { + if (!isDestroyed() && mFile.exists()) { + mFile.delete(); + } + } catch (RuntimeException re) { + // Just log and ignore. + LogUtils.w(TAG, "Failed to remove temp file: " + re.getMessage()); + } + super.destroy(); + } + + @Override + public String toString() { + return String.format("{%d byte literal(file)}", mSize); + } + + public boolean tempFileExistsForTest() { + return mFile.exists(); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java b/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java new file mode 100644 index 000000000..b045eb32f --- /dev/null +++ b/java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.store.imap; + +import com.android.voicemailomtp.mail.utils.LogUtils; + +import java.util.ArrayList; + +/** + * Utility methods for use with IMAP. + */ +public class ImapUtility { + public static final String TAG = "ImapUtility"; + /** + * Apply quoting rules per IMAP RFC, + * quoted = DQUOTE *QUOTED-CHAR DQUOTE + * QUOTED-CHAR = <any TEXT-CHAR except quoted-specials> / "\" quoted-specials + * quoted-specials = DQUOTE / "\" + * + * This is used primarily for IMAP login, but might be useful elsewhere. + * + * NOTE: Not very efficient - you may wish to preflight this, or perhaps it should check + * for trouble chars before calling the replace functions. + * + * @param s The string to be quoted. + * @return A copy of the string, having undergone quoting as described above + */ + public static String imapQuoted(String s) { + + // First, quote any backslashes by replacing \ with \\ + // regex Pattern: \\ (Java string const = \\\\) + // Substitute: \\\\ (Java string const = \\\\\\\\) + String result = s.replaceAll("\\\\", "\\\\\\\\"); + + // Then, quote any double-quotes by replacing " with \" + // regex Pattern: " (Java string const = \") + // Substitute: \\" (Java string const = \\\\\") + result = result.replaceAll("\"", "\\\\\""); + + // return string with quotes around it + return "\"" + result + "\""; + } + + /** + * Gets all of the values in a sequence set per RFC 3501. Any ranges are expanded into a + * list of individual numbers. If the set is invalid, an empty array is returned. + * <pre> + * sequence-number = nz-number / "*" + * sequence-range = sequence-number ":" sequence-number + * sequence-set = (sequence-number / sequence-range) *("," sequence-set) + * </pre> + */ + public static String[] getImapSequenceValues(String set) { + ArrayList<String> list = new ArrayList<String>(); + if (set != null) { + String[] setItems = set.split(","); + for (String item : setItems) { + if (item.indexOf(':') == -1) { + // simple item + try { + Integer.parseInt(item); // Don't need the value; just ensure it's valid + list.add(item); + } catch (NumberFormatException e) { + LogUtils.d(TAG, "Invalid UID value", e); + } + } else { + // range + for (String rangeItem : getImapRangeValues(item)) { + list.add(rangeItem); + } + } + } + } + String[] stringList = new String[list.size()]; + return list.toArray(stringList); + } + + /** + * Expand the given number range into a list of individual numbers. If the range is not valid, + * an empty array is returned. + * <pre> + * sequence-number = nz-number / "*" + * sequence-range = sequence-number ":" sequence-number + * sequence-set = (sequence-number / sequence-range) *("," sequence-set) + * </pre> + */ + public static String[] getImapRangeValues(String range) { + ArrayList<String> list = new ArrayList<String>(); + try { + if (range != null) { + int colonPos = range.indexOf(':'); + if (colonPos > 0) { + int first = Integer.parseInt(range.substring(0, colonPos)); + int second = Integer.parseInt(range.substring(colonPos + 1)); + if (first < second) { + for (int i = first; i <= second; i++) { + list.add(Integer.toString(i)); + } + } else { + for (int i = first; i >= second; i--) { + list.add(Integer.toString(i)); + } + } + } + } + } catch (NumberFormatException e) { + LogUtils.d(TAG, "Invalid range value", e); + } + String[] stringList = new String[list.size()]; + return list.toArray(stringList); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java b/java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java new file mode 100644 index 000000000..fdf81d44a --- /dev/null +++ b/java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.utility; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * A simple pass-thru OutputStream that also counts how many bytes are written to it and + * makes that count available to callers. + */ +public class CountingOutputStream extends OutputStream { + private long mCount; + private final OutputStream mOutputStream; + + public CountingOutputStream(OutputStream outputStream) { + mOutputStream = outputStream; + } + + public long getCount() { + return mCount; + } + + @Override + public void write(byte[] buffer, int offset, int count) throws IOException { + mOutputStream.write(buffer, offset, count); + mCount += count; + } + + @Override + public void write(int oneByte) throws IOException { + mOutputStream.write(oneByte); + mCount++; + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java b/java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java new file mode 100644 index 000000000..5b93a92ab --- /dev/null +++ b/java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.mail.utility; + +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(); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/mail/utils/LogUtils.java b/java/com/android/voicemailomtp/mail/utils/LogUtils.java new file mode 100644 index 000000000..a213a835e --- /dev/null +++ b/java/com/android/voicemailomtp/mail/utils/LogUtils.java @@ -0,0 +1,413 @@ +/** + * Copyright (c) 2015 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.android.voicemailomtp.mail.utils; + +import android.net.Uri; +import android.support.annotation.VisibleForTesting; +import android.text.TextUtils; +import android.util.Log; +import com.android.voicemailomtp.VvmLog; +import java.util.List; +import java.util.regex.Pattern; + +public class LogUtils { + public static final String TAG = "Email Log"; + + // "GMT" + "+" or "-" + 4 digits + private static final Pattern DATE_CLEANUP_PATTERN_WRONG_TIMEZONE = + Pattern.compile("GMT([-+]\\d{4})$"); + + private static final String ACCOUNT_PREFIX = "account:"; + + /** + * Priority constant for the println method; use LogUtils.v. + */ + public static final int VERBOSE = Log.VERBOSE; + + /** + * Priority constant for the println method; use LogUtils.d. + */ + public static final int DEBUG = Log.DEBUG; + + /** + * Priority constant for the println method; use LogUtils.i. + */ + public static final int INFO = Log.INFO; + + /** + * Priority constant for the println method; use LogUtils.w. + */ + public static final int WARN = Log.WARN; + + /** + * Priority constant for the println method; use LogUtils.e. + */ + public static final int ERROR = Log.ERROR; + + /** + * Used to enable/disable logging that we don't want included in production releases. This should + * be set to DEBUG for production releases, and VERBOSE for internal builds. + */ + private static final int MAX_ENABLED_LOG_LEVEL = DEBUG; + + private static Boolean sDebugLoggingEnabledForTests = null; + + /** + * Enable debug logging for unit tests. + */ + @VisibleForTesting + public static void setDebugLoggingEnabledForTests(boolean enabled) { + setDebugLoggingEnabledForTestsInternal(enabled); + } + + protected static void setDebugLoggingEnabledForTestsInternal(boolean enabled) { + sDebugLoggingEnabledForTests = Boolean.valueOf(enabled); + } + + /** + * Returns true if the build configuration prevents debug logging. + */ + @VisibleForTesting + public static boolean buildPreventsDebugLogging() { + return MAX_ENABLED_LOG_LEVEL > VERBOSE; + } + + /** + * Returns a boolean indicating whether debug logging is enabled. + */ + protected static boolean isDebugLoggingEnabled(String tag) { + if (buildPreventsDebugLogging()) { + return false; + } + if (sDebugLoggingEnabledForTests != null) { + return sDebugLoggingEnabledForTests.booleanValue(); + } + return Log.isLoggable(tag, Log.DEBUG) || Log.isLoggable(TAG, Log.DEBUG); + } + + /** + * Returns a String for the specified content provider uri. This will do + * sanitation of the uri to remove PII if debug logging is not enabled. + */ + public static String contentUriToString(final Uri uri) { + return contentUriToString(TAG, uri); + } + + /** + * Returns a String for the specified content provider uri. This will do + * sanitation of the uri to remove PII if debug logging is not enabled. + */ + public static String contentUriToString(String tag, Uri uri) { + if (isDebugLoggingEnabled(tag)) { + // Debug logging has been enabled, so log the uri as is + return uri.toString(); + } else { + // Debug logging is not enabled, we want to remove the email address from the uri. + List<String> pathSegments = uri.getPathSegments(); + + Uri.Builder builder = new Uri.Builder() + .scheme(uri.getScheme()) + .authority(uri.getAuthority()) + .query(uri.getQuery()) + .fragment(uri.getFragment()); + + // This assumes that the first path segment is the account + final String account = pathSegments.get(0); + + builder = builder.appendPath(sanitizeAccountName(account)); + for (int i = 1; i < pathSegments.size(); i++) { + builder.appendPath(pathSegments.get(i)); + } + return builder.toString(); + } + } + + /** + * Sanitizes an account name. If debug logging is not enabled, a sanitized name + * is returned. + */ + public static String sanitizeAccountName(String accountName) { + if (TextUtils.isEmpty(accountName)) { + return ""; + } + + return ACCOUNT_PREFIX + sanitizeName(TAG, accountName); + } + + public static String sanitizeName(final String tag, final String name) { + if (TextUtils.isEmpty(name)) { + return ""; + } + + if (isDebugLoggingEnabled(tag)) { + return name; + } + + return String.valueOf(name.hashCode()); + } + + /** + * Checks to see whether or not a log for the specified tag is loggable at the specified level. + */ + public static boolean isLoggable(String tag, int level) { + if (MAX_ENABLED_LOG_LEVEL > level) { + return false; + } + return Log.isLoggable(tag, level) || Log.isLoggable(TAG, level); + } + + /** + * Send a {@link #VERBOSE} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int v(String tag, String format, Object... args) { + if (isLoggable(tag, VERBOSE)) { + return VvmLog.v(tag, String.format(format, args)); + } + return 0; + } + + /** + * Send a {@link #VERBOSE} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param tr An exception to log + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int v(String tag, Throwable tr, String format, Object... args) { + if (isLoggable(tag, VERBOSE)) { + return VvmLog.v(tag, String.format(format, args), tr); + } + return 0; + } + + /** + * Send a {@link #DEBUG} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int d(String tag, String format, Object... args) { + if (isLoggable(tag, DEBUG)) { + return VvmLog.d(tag, String.format(format, args)); + } + return 0; + } + + /** + * Send a {@link #DEBUG} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param tr An exception to log + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int d(String tag, Throwable tr, String format, Object... args) { + if (isLoggable(tag, DEBUG)) { + return VvmLog.d(tag, String.format(format, args), tr); + } + return 0; + } + + /** + * Send a {@link #INFO} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int i(String tag, String format, Object... args) { + if (isLoggable(tag, INFO)) { + return VvmLog.i(tag, String.format(format, args)); + } + return 0; + } + + /** + * Send a {@link #INFO} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param tr An exception to log + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int i(String tag, Throwable tr, String format, Object... args) { + if (isLoggable(tag, INFO)) { + return VvmLog.i(tag, String.format(format, args), tr); + } + return 0; + } + + /** + * Send a {@link #WARN} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int w(String tag, String format, Object... args) { + if (isLoggable(tag, WARN)) { + return VvmLog.w(tag, String.format(format, args)); + } + return 0; + } + + /** + * Send a {@link #WARN} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param tr An exception to log + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int w(String tag, Throwable tr, String format, Object... args) { + if (isLoggable(tag, WARN)) { + return VvmLog.w(tag, String.format(format, args), tr); + } + return 0; + } + + /** + * Send a {@link #ERROR} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int e(String tag, String format, Object... args) { + if (isLoggable(tag, ERROR)) { + return VvmLog.e(tag, String.format(format, args)); + } + return 0; + } + + /** + * Send a {@link #ERROR} log message. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param tr An exception to log + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int e(String tag, Throwable tr, String format, Object... args) { + if (isLoggable(tag, ERROR)) { + return VvmLog.e(tag, String.format(format, args), tr); + } + return 0; + } + + /** + * What a Terrible Failure: Report a condition that should never happen. + * The error will always be logged at level ASSERT with the call stack. + * Depending on system configuration, a report may be added to the + * {@link android.os.DropBoxManager} and/or the process may be terminated + * immediately with an error dialog. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int wtf(String tag, String format, Object... args) { + return VvmLog.wtf(tag, String.format(format, args), new Error()); + } + + /** + * What a Terrible Failure: Report a condition that should never happen. + * The error will always be logged at level ASSERT with the call stack. + * Depending on system configuration, a report may be added to the + * {@link android.os.DropBoxManager} and/or the process may be terminated + * immediately with an error dialog. + * @param tag Used to identify the source of a log message. It usually identifies + * the class or activity where the log call occurs. + * @param tr An exception to log + * @param format the format string (see {@link java.util.Formatter#format}) + * @param args + * the list of arguments passed to the formatter. If there are + * more arguments than required by {@code format}, + * additional arguments are ignored. + */ + public static int wtf(String tag, Throwable tr, String format, Object... args) { + return VvmLog.wtf(tag, String.format(format, args), tr); + } + + + /** + * Try to make a date MIME(RFC 2822/5322)-compliant. + * + * It fixes: + * - "Thu, 10 Dec 09 15:08:08 GMT-0700" to "Thu, 10 Dec 09 15:08:08 -0700" + * (4 digit zone value can't be preceded by "GMT") + * We got a report saying eBay sends a date in this format + */ + public static String cleanUpMimeDate(String date) { + if (TextUtils.isEmpty(date)) { + return date; + } + date = DATE_CLEANUP_PATTERN_WRONG_TIMEZONE.matcher(date).replaceFirst("$1"); + return date; + } + + + public static String byteToHex(int b) { + return byteToHex(new StringBuilder(), b).toString(); + } + + public static StringBuilder byteToHex(StringBuilder sb, int b) { + b &= 0xFF; + sb.append("0123456789ABCDEF".charAt(b >> 4)); + sb.append("0123456789ABCDEF".charAt(b & 0xF)); + return sb; + } + +} diff --git a/java/com/android/voicemailomtp/mail/utils/Utility.java b/java/com/android/voicemailomtp/mail/utils/Utility.java new file mode 100644 index 000000000..c7286fa64 --- /dev/null +++ b/java/com/android/voicemailomtp/mail/utils/Utility.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2015 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.android.voicemailomtp.mail.utils; + +import java.io.ByteArrayInputStream; +import java.nio.ByteBuffer; +import java.nio.CharBuffer; +import java.nio.charset.Charset; + +/** + * Simple utility methods used in email functions. + */ +public class Utility { + public static final Charset ASCII = Charset.forName("US-ASCII"); + + public static final String[] EMPTY_STRINGS = new String[0]; + + /** + * Returns a concatenated string containing the output of every Object's + * toString() method, each separated by the given separator character. + */ + public static String combine(Object[] parts, char separator) { + if (parts == null) { + return null; + } + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < parts.length; i++) { + sb.append(parts[i].toString()); + if (i < parts.length - 1) { + sb.append(separator); + } + } + return sb.toString(); + } + + /** Converts a String to ASCII bytes */ + public static byte[] toAscii(String s) { + return encode(ASCII, s); + } + + /** Builds a String from ASCII bytes */ + public static String fromAscii(byte[] b) { + return decode(ASCII, b); + } + + private static byte[] encode(Charset charset, String s) { + if (s == null) { + return null; + } + final ByteBuffer buffer = charset.encode(CharBuffer.wrap(s)); + final byte[] bytes = new byte[buffer.limit()]; + buffer.get(bytes); + return bytes; + } + + private static String decode(Charset charset, byte[] b) { + if (b == null) { + return null; + } + final CharBuffer cb = charset.decode(ByteBuffer.wrap(b)); + return new String(cb.array(), 0, cb.length()); + } + + public static ByteArrayInputStream streamFromAsciiString(String ascii) { + return new ByteArrayInputStream(toAscii(ascii)); + } +} diff --git a/java/com/android/voicemailomtp/permissions.xml b/java/com/android/voicemailomtp/permissions.xml new file mode 100644 index 000000000..9326d803a --- /dev/null +++ b/java/com/android/voicemailomtp/permissions.xml @@ -0,0 +1,21 @@ +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.voicemailomtp"> + + <uses-sdk + android:minSdkVersion="23" + android:targetSdkVersion="25"/> + + <!-- Applications using this module should merge these permissions using android_manifest_merge --> + + <uses-permission android:name="com.android.voicemail.permission.ADD_VOICEMAIL" /> + <uses-permission android:name="com.android.voicemail.permission.WRITE_VOICEMAIL" /> + <uses-permission android:name="com.android.voicemail.permission.READ_VOICEMAIL" /> + <uses-permission android:name="android.permission.WAKE_LOCK"/> + <uses-permission android:name="android.permission.READ_PHONE_STATE"/> + <uses-permission android:name="android.permission.SEND_SMS"/> + <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE"/> + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> + + <application/> +</manifest> diff --git a/java/com/android/voicemailomtp/protocol/CvvmProtocol.java b/java/com/android/voicemailomtp/protocol/CvvmProtocol.java new file mode 100644 index 000000000..48ed99709 --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/CvvmProtocol.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.protocol; + +import android.content.Context; +import android.telecom.PhoneAccountHandle; +import android.telephony.SmsManager; + +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.sms.OmtpCvvmMessageSender; +import com.android.voicemailomtp.sms.OmtpMessageSender; + +/** + * A flavor of OMTP protocol with a different mobile originated (MO) format + * + * Used by carriers such as T-Mobile + */ +public class CvvmProtocol extends VisualVoicemailProtocol { + + private static String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s"; + private static String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s"; + private static String IMAP_CLOSE_NUT = "CLOSE_NUT"; + + @Override + public OmtpMessageSender createMessageSender(Context context, + PhoneAccountHandle phoneAccountHandle, short applicationPort, + String destinationNumber) { + return new OmtpCvvmMessageSender(context, phoneAccountHandle, applicationPort, + destinationNumber); + } + + @Override + public String getCommand(String command) { + if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) { + return IMAP_CHANGE_TUI_PWD_FORMAT; + } + if (command == OmtpConstants.IMAP_CLOSE_NUT) { + return IMAP_CLOSE_NUT; + } + if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) { + return IMAP_CHANGE_VM_LANG_FORMAT; + } + return super.getCommand(command); + } +} diff --git a/java/com/android/voicemailomtp/protocol/OmtpProtocol.java b/java/com/android/voicemailomtp/protocol/OmtpProtocol.java new file mode 100644 index 000000000..d88a23285 --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/OmtpProtocol.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.protocol; + +import android.content.Context; +import android.telecom.PhoneAccountHandle; +import android.telephony.SmsManager; + +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.sms.OmtpMessageSender; +import com.android.voicemailomtp.sms.OmtpStandardMessageSender; + +public class OmtpProtocol extends VisualVoicemailProtocol { + + @Override + public OmtpMessageSender createMessageSender(Context context, + PhoneAccountHandle phoneAccountHandle, short applicationPort, + String destinationNumber) { + return new OmtpStandardMessageSender(context, phoneAccountHandle, applicationPort, + destinationNumber, + null, OmtpConstants.PROTOCOL_VERSION1_1, null); + } +} diff --git a/java/com/android/voicemailomtp/protocol/ProtocolHelper.java b/java/com/android/voicemailomtp/protocol/ProtocolHelper.java new file mode 100644 index 000000000..4fca199bf --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/ProtocolHelper.java @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.protocol; + +import android.telephony.SmsManager; +import android.text.TextUtils; + +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.sms.OmtpMessageSender; + +public class ProtocolHelper { + + private static final String TAG = "ProtocolHelper"; + + public static OmtpMessageSender getMessageSender(VisualVoicemailProtocol protocol, + OmtpVvmCarrierConfigHelper config) { + + int applicationPort = config.getApplicationPort(); + String destinationNumber = config.getDestinationNumber(); + if (TextUtils.isEmpty(destinationNumber)) { + VvmLog.w(TAG, "No destination number for this carrier."); + return null; + } + + return protocol.createMessageSender(config.getContext(), config.getPhoneAccountHandle(), + (short) applicationPort, destinationNumber); + } +} diff --git a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java new file mode 100644 index 000000000..9ff2ed167 --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.protocol; + +import android.app.PendingIntent; +import android.content.Context; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.telephony.SmsManager; +import com.android.voicemailomtp.ActivationTask; +import com.android.voicemailomtp.DefaultOmtpEventHandler; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.sms.OmtpMessageSender; +import com.android.voicemailomtp.sms.StatusMessage; + +public abstract class VisualVoicemailProtocol { + + /** + * Activation should cause the carrier to respond with a STATUS SMS. + */ + public void startActivation(OmtpVvmCarrierConfigHelper config, PendingIntent sentIntent) { + OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config); + if (messageSender != null) { + messageSender.requestVvmActivation(sentIntent); + } + } + + public void startDeactivation(OmtpVvmCarrierConfigHelper config) { + OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config); + if (messageSender != null) { + messageSender.requestVvmDeactivation(null); + } + } + + public boolean supportsProvisioning() { + return false; + } + + public void startProvisioning(ActivationTask task, PhoneAccountHandle handle, + OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor editor, StatusMessage message, + Bundle data) { + // Do nothing + } + + public void requestStatus(OmtpVvmCarrierConfigHelper config, + @Nullable PendingIntent sentIntent) { + OmtpMessageSender messageSender = ProtocolHelper.getMessageSender(this, config); + if (messageSender != null) { + messageSender.requestVvmStatus(sentIntent); + } + } + + public abstract OmtpMessageSender createMessageSender(Context context, + PhoneAccountHandle phoneAccountHandle, + short applicationPort, String destinationNumber); + + /** + * Translate an OMTP IMAP command to the protocol specific one. For example, changing the TUI + * password on OMTP is XCHANGE_TUI_PWD, but on CVVM and VVM3 it is CHANGE_TUI_PWD. + * + * @param command A String command in {@link com.android.voicemailomtp.OmtpConstants}, the exact + * instance should be used instead of its' value. + * @returns Translated command, or {@code null} if not available in this protocol + */ + public String getCommand(String command) { + return command; + } + + public void handleEvent(Context context, OmtpVvmCarrierConfigHelper config, + VoicemailStatus.Editor status, OmtpEvents event) { + DefaultOmtpEventHandler.handleEvent(context, config, status, event); + } + + /** + * Given an VVM SMS with an unknown {@code event}, let the protocol attempt to translate it into + * an equivalent STATUS SMS. Returns {@code null} if it cannot be translated. + */ + @Nullable + public Bundle translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event, + Bundle data) { + return null; + } +} diff --git a/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java new file mode 100644 index 000000000..b74f503c6 --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.protocol; + +import android.content.res.Resources; +import android.support.annotation.Nullable; +import android.telephony.TelephonyManager; +import com.android.voicemailomtp.VvmLog; + +public class VisualVoicemailProtocolFactory { + + private static final String TAG = "VvmProtocolFactory"; + + private static final String VVM_TYPE_VVM3 = "vvm_type_vvm3"; + + @Nullable + public static VisualVoicemailProtocol create(Resources resources, String type) { + if (type == null) { + return null; + } + switch (type) { + case TelephonyManager.VVM_TYPE_OMTP: + return new OmtpProtocol(); + case TelephonyManager.VVM_TYPE_CVVM: + return new CvvmProtocol(); + case VVM_TYPE_VVM3: + return new Vvm3Protocol(); + default: + VvmLog.e(TAG, "Unexpected visual voicemail type: " + type); + } + return null; + } +} diff --git a/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java b/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java new file mode 100644 index 000000000..72646386c --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.protocol; + +import android.content.Context; +import android.support.annotation.IntDef; +import android.util.Log; +import com.android.voicemailomtp.DefaultOmtpEventHandler; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpEvents.Type; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.settings.VoicemailChangePinActivity; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Handles {@link OmtpEvents} when {@link Vvm3Protocol} is being used. This handler writes custom + * error codes into the voicemail status table so support on the dialer side is required. + * + * TODO(b/29577838) disable VVM3 by default so support on system dialer can be ensured. + */ +public class Vvm3EventHandler { + + private static final String TAG = "Vvm3EventHandler"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({VMS_DNS_FAILURE, VMG_DNS_FAILURE, SPG_DNS_FAILURE, VMS_NO_CELLULAR, VMG_NO_CELLULAR, + SPG_NO_CELLULAR, VMS_TIMEOUT, VMG_TIMEOUT, STATUS_SMS_TIMEOUT, SUBSCRIBER_BLOCKED, + UNKNOWN_USER, UNKNOWN_DEVICE, INVALID_PASSWORD, MAILBOX_NOT_INITIALIZED, + SERVICE_NOT_PROVISIONED, SERVICE_NOT_ACTIVATED, USER_BLOCKED, IMAP_GETQUOTA_ERROR, + IMAP_SELECT_ERROR, IMAP_ERROR, VMG_INTERNAL_ERROR, VMG_DB_ERROR, + VMG_COMMUNICATION_ERROR, SPG_URL_NOT_FOUND, VMG_UNKNOWN_ERROR, PIN_NOT_SET}) + public @interface ErrorCode { + + } + + public static final int VMS_DNS_FAILURE = -9001; + public static final int VMG_DNS_FAILURE = -9002; + public static final int SPG_DNS_FAILURE = -9003; + public static final int VMS_NO_CELLULAR = -9004; + public static final int VMG_NO_CELLULAR = -9005; + public static final int SPG_NO_CELLULAR = -9006; + public static final int VMS_TIMEOUT = -9007; + public static final int VMG_TIMEOUT = -9008; + public static final int STATUS_SMS_TIMEOUT = -9009; + + public static final int SUBSCRIBER_BLOCKED = -9990; + public static final int UNKNOWN_USER = -9991; + public static final int UNKNOWN_DEVICE = -9992; + public static final int INVALID_PASSWORD = -9993; + public static final int MAILBOX_NOT_INITIALIZED = -9994; + public static final int SERVICE_NOT_PROVISIONED = -9995; + public static final int SERVICE_NOT_ACTIVATED = -9996; + public static final int USER_BLOCKED = -9998; + public static final int IMAP_GETQUOTA_ERROR = -9997; + public static final int IMAP_SELECT_ERROR = -9989; + public static final int IMAP_ERROR = -9999; + + public static final int VMG_INTERNAL_ERROR = -101; + public static final int VMG_DB_ERROR = -102; + public static final int VMG_COMMUNICATION_ERROR = -103; + public static final int SPG_URL_NOT_FOUND = -301; + + // Non VVM3 codes: + public static final int VMG_UNKNOWN_ERROR = -1; + public static final int PIN_NOT_SET = -100; + // STATUS SMS returned st=U and rc!=2. The user cannot be provisioned and must contact customer + // support. + public static final int SUBSCRIBER_UNKNOWN = -99; + + + public static void handleEvent(Context context, OmtpVvmCarrierConfigHelper config, + VoicemailStatus.Editor status, OmtpEvents event) { + boolean handled = false; + switch (event.getType()) { + case Type.CONFIGURATION: + handled = handleConfigurationEvent(context, status, event); + break; + case Type.DATA_CHANNEL: + handled = handleDataChannelEvent(status, event); + break; + case Type.NOTIFICATION_CHANNEL: + handled = handleNotificationChannelEvent(status, event); + break; + case Type.OTHER: + handled = handleOtherEvent(status, event); + break; + default: + Log.wtf(TAG, "invalid event type " + event.getType() + " for " + event); + } + if (!handled) { + DefaultOmtpEventHandler.handleEvent(context, config, status, event); + } + } + + private static boolean handleConfigurationEvent(Context context, VoicemailStatus.Editor status, + OmtpEvents event) { + switch (event) { + case CONFIG_REQUEST_STATUS_SUCCESS: + if (status.getPhoneAccountHandle() == null) { + // This should never happen. + Log.e(TAG, "status editor has null phone account handle"); + return true; + } + + if (!VoicemailChangePinActivity + .isDefaultOldPinSet(context, status.getPhoneAccountHandle())) { + return false; + } else { + postError(status, PIN_NOT_SET); + } + break; + case CONFIG_DEFAULT_PIN_REPLACED: + postError(status, PIN_NOT_SET); + break; + case CONFIG_STATUS_SMS_TIME_OUT: + postError(status, STATUS_SMS_TIMEOUT); + break; + default: + return false; + } + return true; + } + + private static boolean handleDataChannelEvent(VoicemailStatus.Editor status, OmtpEvents event) { + switch (event) { + case DATA_NO_CONNECTION: + case DATA_NO_CONNECTION_CELLULAR_REQUIRED: + case DATA_ALL_SOCKET_CONNECTION_FAILED: + postError(status, VMS_NO_CELLULAR); + break; + case DATA_SSL_INVALID_HOST_NAME: + case DATA_CANNOT_ESTABLISH_SSL_SESSION: + case DATA_IOE_ON_OPEN: + postError(status, VMS_TIMEOUT); + break; + case DATA_CANNOT_RESOLVE_HOST_ON_NETWORK: + postError(status, VMS_DNS_FAILURE); + break; + case DATA_BAD_IMAP_CREDENTIAL: + postError(status, IMAP_ERROR); + break; + case DATA_AUTH_UNKNOWN_USER: + postError(status, UNKNOWN_USER); + break; + case DATA_AUTH_UNKNOWN_DEVICE: + postError(status, UNKNOWN_DEVICE); + break; + case DATA_AUTH_INVALID_PASSWORD: + postError(status, INVALID_PASSWORD); + break; + case DATA_AUTH_MAILBOX_NOT_INITIALIZED: + postError(status, MAILBOX_NOT_INITIALIZED); + break; + case DATA_AUTH_SERVICE_NOT_PROVISIONED: + postError(status, SERVICE_NOT_PROVISIONED); + break; + case DATA_AUTH_SERVICE_NOT_ACTIVATED: + postError(status, SERVICE_NOT_ACTIVATED); + break; + case DATA_AUTH_USER_IS_BLOCKED: + postError(status, USER_BLOCKED); + break; + case DATA_REJECTED_SERVER_RESPONSE: + case DATA_INVALID_INITIAL_SERVER_RESPONSE: + case DATA_SSL_EXCEPTION: + postError(status, IMAP_ERROR); + break; + default: + return false; + } + return true; + } + + private static boolean handleNotificationChannelEvent(VoicemailStatus.Editor status, + OmtpEvents event) { + return false; + } + + private static boolean handleOtherEvent(VoicemailStatus.Editor status, + OmtpEvents event) { + switch (event) { + case VVM3_NEW_USER_SETUP_FAILED: + postError(status, MAILBOX_NOT_INITIALIZED); + break; + case VVM3_VMG_DNS_FAILURE: + postError(status, VMG_DNS_FAILURE); + break; + case VVM3_SPG_DNS_FAILURE: + postError(status, SPG_DNS_FAILURE); + break; + case VVM3_VMG_CONNECTION_FAILED: + postError(status, VMG_NO_CELLULAR); + break; + case VVM3_SPG_CONNECTION_FAILED: + postError(status, SPG_NO_CELLULAR); + break; + case VVM3_VMG_TIMEOUT: + postError(status, VMG_TIMEOUT); + break; + case VVM3_SUBSCRIBER_PROVISIONED: + postError(status, SERVICE_NOT_ACTIVATED); + break; + case VVM3_SUBSCRIBER_BLOCKED: + postError(status, SUBSCRIBER_BLOCKED); + break; + case VVM3_SUBSCRIBER_UNKNOWN: + postError(status, SUBSCRIBER_UNKNOWN); + break; + default: + return false; + } + return true; + } + + private static void postError(VoicemailStatus.Editor editor, @ErrorCode int errorCode) { + switch (errorCode) { + case VMG_DNS_FAILURE: + case SPG_DNS_FAILURE: + case VMG_NO_CELLULAR: + case SPG_NO_CELLULAR: + case VMG_TIMEOUT: + case SUBSCRIBER_BLOCKED: + case UNKNOWN_USER: + case UNKNOWN_DEVICE: + case INVALID_PASSWORD: + case MAILBOX_NOT_INITIALIZED: + case SERVICE_NOT_PROVISIONED: + case SERVICE_NOT_ACTIVATED: + case USER_BLOCKED: + case VMG_UNKNOWN_ERROR: + case SPG_URL_NOT_FOUND: + case VMG_INTERNAL_ERROR: + case VMG_DB_ERROR: + case VMG_COMMUNICATION_ERROR: + case PIN_NOT_SET: + case SUBSCRIBER_UNKNOWN: + editor.setConfigurationState(errorCode); + break; + case VMS_NO_CELLULAR: + case VMS_DNS_FAILURE: + case VMS_TIMEOUT: + case IMAP_GETQUOTA_ERROR: + case IMAP_SELECT_ERROR: + case IMAP_ERROR: + editor.setDataChannelState(errorCode); + break; + case STATUS_SMS_TIMEOUT: + editor.setNotificationChannelState(errorCode); + break; + default: + Log.wtf(TAG, "unknown error code: " + errorCode); + } + editor.apply(); + } +} diff --git a/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java b/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java new file mode 100644 index 000000000..652d1010a --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/Vvm3Protocol.java @@ -0,0 +1,301 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.protocol; + +import android.annotation.TargetApi; +import android.app.PendingIntent; +import android.content.Context; +import android.net.Network; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import com.android.voicemailomtp.ActivationTask; +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VisualVoicemailPreferences; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.imap.ImapHelper; +import com.android.voicemailomtp.imap.ImapHelper.InitializingException; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil; +import com.android.voicemailomtp.settings.VoicemailChangePinActivity; +import com.android.voicemailomtp.sms.OmtpMessageSender; +import com.android.voicemailomtp.sms.StatusMessage; +import com.android.voicemailomtp.sms.Vvm3MessageSender; +import com.android.voicemailomtp.sync.VvmNetworkRequest; +import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper; +import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Locale; + +/** + * A flavor of OMTP protocol with a different provisioning process + * + * <p>Used by carriers such as Verizon Wireless + */ +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class Vvm3Protocol extends VisualVoicemailProtocol { + + private static final String TAG = "Vvm3Protocol"; + + private static final String SMS_EVENT_UNRECOGNIZED = "UNRECOGNIZED"; + private static final String SMS_EVENT_UNRECOGNIZED_CMD = "cmd"; + private static final String SMS_EVENT_UNRECOGNIZED_STATUS = "STATUS"; + private static final String DEFAULT_VMG_URL_KEY = "default_vmg_url"; + + private static final String IMAP_CHANGE_TUI_PWD_FORMAT = "CHANGE_TUI_PWD PWD=%1$s OLD_PWD=%2$s"; + private static final String IMAP_CHANGE_VM_LANG_FORMAT = "CHANGE_VM_LANG Lang=%1$s"; + private static final String IMAP_CLOSE_NUT = "CLOSE_NUT"; + + private static final String ISO639_Spanish = "es"; + + /** + * For VVM3, if the STATUS SMS returns {@link StatusMessage#getProvisioningStatus()} of {@link + * OmtpConstants#SUBSCRIBER_UNKNOWN} and {@link StatusMessage#getReturnCode()} of this value, + * the user can self-provision visual voicemail service. For other response codes, the user must + * contact customer support to resolve the issue. + */ + private static final String VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE = "2"; + + // Default prompt level when using the telephone user interface. + // Standard prompt when the user call into the voicemail, and no prompts when someone else is + // leaving a voicemail. + private static final String VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS = "5"; + private static final String VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS = "6"; + + private static final int DEFAULT_PIN_LENGTH = 6; + + @Override + public void startActivation(OmtpVvmCarrierConfigHelper config, + @Nullable PendingIntent sentIntent) { + // VVM3 does not support activation SMS. + // Send a status request which will start the provisioning process if the user is not + // provisioned. + VvmLog.i(TAG, "Activating"); + config.requestStatus(sentIntent); + } + + @Override + public void startDeactivation(OmtpVvmCarrierConfigHelper config) { + // VVM3 does not support deactivation. + // do nothing. + } + + @Override + public boolean supportsProvisioning() { + return true; + } + + @Override + public void startProvisioning(ActivationTask task, PhoneAccountHandle phoneAccountHandle, + OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, StatusMessage message, + Bundle data) { + VvmLog.i(TAG, "start vvm3 provisioning"); + if (OmtpConstants.SUBSCRIBER_UNKNOWN.equals(message.getProvisioningStatus())) { + VvmLog.i(TAG, "Provisioning status: Unknown"); + if (VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE + .equals(message.getReturnCode())) { + VvmLog.i(TAG, "Self provisioning available, subscribing"); + new Vvm3Subscriber(task, phoneAccountHandle, config, status, data).subscribe(); + } else { + config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_UNKNOWN); + } + } else if (OmtpConstants.SUBSCRIBER_NEW.equals(message.getProvisioningStatus())) { + VvmLog.i(TAG, "setting up new user"); + // Save the IMAP credentials in preferences so they are persistent and can be retrieved. + VisualVoicemailPreferences prefs = + new VisualVoicemailPreferences(config.getContext(), phoneAccountHandle); + message.putStatus(prefs.edit()).apply(); + + startProvisionNewUser(task, phoneAccountHandle, config, status, message); + } else if (OmtpConstants.SUBSCRIBER_PROVISIONED.equals(message.getProvisioningStatus())) { + VvmLog.i(TAG, "User provisioned but not activated, disabling VVM"); + VisualVoicemailSettingsUtil + .setEnabled(config.getContext(), phoneAccountHandle, false); + } else if (OmtpConstants.SUBSCRIBER_BLOCKED.equals(message.getProvisioningStatus())) { + VvmLog.i(TAG, "User blocked"); + config.handleEvent(status, OmtpEvents.VVM3_SUBSCRIBER_BLOCKED); + } + } + + @Override + public OmtpMessageSender createMessageSender(Context context, + PhoneAccountHandle phoneAccountHandle, short applicationPort, + String destinationNumber) { + return new Vvm3MessageSender(context, phoneAccountHandle, applicationPort, + destinationNumber); + } + + @Override + public void handleEvent(Context context, OmtpVvmCarrierConfigHelper config, + VoicemailStatus.Editor status, OmtpEvents event) { + Vvm3EventHandler.handleEvent(context, config, status, event); + } + + @Override + public String getCommand(String command) { + if (command == OmtpConstants.IMAP_CHANGE_TUI_PWD_FORMAT) { + return IMAP_CHANGE_TUI_PWD_FORMAT; + } + if (command == OmtpConstants.IMAP_CLOSE_NUT) { + return IMAP_CLOSE_NUT; + } + if (command == OmtpConstants.IMAP_CHANGE_VM_LANG_FORMAT) { + return IMAP_CHANGE_VM_LANG_FORMAT; + } + return super.getCommand(command); + } + + @Override + public Bundle translateStatusSmsBundle(OmtpVvmCarrierConfigHelper config, String event, + Bundle data) { + // UNRECOGNIZED?cmd=STATUS is the response of a STATUS request when the user is provisioned + // with iPhone visual voicemail without VoLTE. Translate it into an unprovisioned status + // so provisioning can be done. + if (!SMS_EVENT_UNRECOGNIZED.equals(event)) { + return null; + } + if (!SMS_EVENT_UNRECOGNIZED_STATUS.equals(data.getString(SMS_EVENT_UNRECOGNIZED_CMD))) { + return null; + } + Bundle bundle = new Bundle(); + bundle.putString(OmtpConstants.PROVISIONING_STATUS, OmtpConstants.SUBSCRIBER_UNKNOWN); + bundle.putString(OmtpConstants.RETURN_CODE, + VVM3_UNKNOWN_SUBSCRIBER_CAN_SUBSCRIBE_RESPONSE_CODE); + String vmgUrl = config.getString(DEFAULT_VMG_URL_KEY); + if (TextUtils.isEmpty(vmgUrl)) { + VvmLog.e(TAG, "Unable to translate STATUS SMS: VMG URL is not set in config"); + return null; + } + bundle.putString(Vvm3Subscriber.VMG_URL_KEY, vmgUrl); + VvmLog.i(TAG, "UNRECOGNIZED?cmd=STATUS translated into unprovisioned STATUS SMS"); + return bundle; + } + + private void startProvisionNewUser(ActivationTask task, PhoneAccountHandle phoneAccountHandle, + OmtpVvmCarrierConfigHelper config, VoicemailStatus.Editor status, + StatusMessage message) { + try (NetworkWrapper wrapper = VvmNetworkRequest + .getNetwork(config, phoneAccountHandle, status)) { + Network network = wrapper.get(); + + VvmLog.i(TAG, "new user: network available"); + try (ImapHelper helper = new ImapHelper(config.getContext(), phoneAccountHandle, + network, status)) { + // VVM3 has inconsistent error language code to OMTP. Just issue a raw command + // here. + // TODO(b/29082671): use LocaleList + if (Locale.getDefault().getLanguage() + .equals(new Locale(ISO639_Spanish).getLanguage())) { + // Spanish + helper.changeVoicemailTuiLanguage( + VVM3_VM_LANGUAGE_SPANISH_STANDARD_NO_GUEST_PROMPTS); + } else { + // English + helper.changeVoicemailTuiLanguage( + VVM3_VM_LANGUAGE_ENGLISH_STANDARD_NO_GUEST_PROMPTS); + } + VvmLog.i(TAG, "new user: language set"); + + if (setPin(config.getContext(), phoneAccountHandle, helper, message)) { + // Only close new user tutorial if the PIN has been changed. + helper.closeNewUserTutorial(); + VvmLog.i(TAG, "new user: NUT closed"); + + config.requestStatus(null); + } + } catch (InitializingException | MessagingException | IOException e) { + config.handleEvent(status, OmtpEvents.VVM3_NEW_USER_SETUP_FAILED); + task.fail(); + VvmLog.e(TAG, e.toString()); + } + } catch (RequestFailedException e) { + config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED); + task.fail(); + } + + } + + + private static boolean setPin(Context context, PhoneAccountHandle phoneAccountHandle, + ImapHelper helper, StatusMessage message) + throws IOException, MessagingException { + String defaultPin = getDefaultPin(message); + if (defaultPin == null) { + VvmLog.i(TAG, "cannot generate default PIN"); + return false; + } + + if (VoicemailChangePinActivity.isDefaultOldPinSet(context, phoneAccountHandle)) { + // The pin was already set + VvmLog.i(TAG, "PIN already set"); + return true; + } + String newPin = generatePin(getMinimumPinLength(context, phoneAccountHandle)); + if (helper.changePin(defaultPin, newPin) == OmtpConstants.CHANGE_PIN_SUCCESS) { + VoicemailChangePinActivity.setDefaultOldPIN(context, phoneAccountHandle, newPin); + helper.handleEvent(OmtpEvents.CONFIG_DEFAULT_PIN_REPLACED); + } + VvmLog.i(TAG, "new user: PIN set"); + return true; + } + + @Nullable + private static String getDefaultPin(StatusMessage message) { + // The IMAP username is [phone number]@example.com + String username = message.getImapUserName(); + try { + String number = username.substring(0, username.indexOf('@')); + if (number.length() < 4) { + VvmLog.e(TAG, "unable to extract number from IMAP username"); + return null; + } + return "1" + number.substring(number.length() - 4); + } catch (StringIndexOutOfBoundsException e) { + VvmLog.e(TAG, "unable to extract number from IMAP username"); + return null; + } + + } + + private static int getMinimumPinLength(Context context, PhoneAccountHandle phoneAccountHandle) { + VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(context, + phoneAccountHandle); + // The OMTP pin length format is {min}-{max} + String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-"); + if (lengths.length == 2) { + try { + return Integer.parseInt(lengths[0]); + } catch (NumberFormatException e) { + return DEFAULT_PIN_LENGTH; + } + } + return DEFAULT_PIN_LENGTH; + } + + private static String generatePin(int length) { + SecureRandom random = new SecureRandom(); + return String.format(Locale.US, "%010d", Math.abs(random.nextLong())) + .substring(0, length); + + } +} diff --git a/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java b/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java new file mode 100644 index 000000000..0a4d792b2 --- /dev/null +++ b/java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.protocol; + +import android.annotation.TargetApi; +import android.net.Network; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.annotation.WorkerThread; +import android.telecom.PhoneAccountHandle; +import android.telephony.TelephonyManager; +import android.text.Html; +import android.text.Spanned; +import android.text.style.URLSpan; +import android.util.ArrayMap; +import com.android.voicemailomtp.ActivationTask; +import com.android.voicemailomtp.Assert; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.sync.VvmNetworkRequest; +import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper; +import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException; +import com.android.volley.AuthFailureError; +import com.android.volley.Request; +import com.android.volley.RequestQueue; +import com.android.volley.toolbox.HurlStack; +import com.android.volley.toolbox.RequestFuture; +import com.android.volley.toolbox.StringRequest; +import com.android.volley.toolbox.Volley; +import java.io.IOException; +import java.net.CookieHandler; +import java.net.CookieManager; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Locale; +import java.util.Map; +import java.util.Random; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Class to subscribe to basic VVM3 visual voicemail, for example, Verizon. Subscription is required + * when the user is unprovisioned. This could happen when the user is on a legacy service, or + * switched over from devices that used other type of visual voicemail. + * + * <p>The STATUS SMS will come with a URL to the voicemail management gateway. From it we can find + * the self provisioning gateway URL that we can modify voicemail services. + * + * <p>A request to the self provisioning gateway to activate basic visual voicemail will return us + * with a web page. If the user hasn't subscribe to it yet it will contain a link to confirm the + * subscription. This link should be clicked through cellular network, and have cookies enabled. + * + * <p>After the process is completed, the carrier should send us another STATUS SMS with a new or + * ready user. + */ +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class Vvm3Subscriber { + + private static final String TAG = "Vvm3Subscriber"; + + private static final String OPERATION_GET_SPG_URL = "retrieveSPGURL"; + private static final String SPG_URL_TAG = "spgurl"; + private static final String TRANSACTION_ID_TAG = "transactionid"; + //language=XML + private static final String VMG_XML_REQUEST_FORMAT = "" + + "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" + + "<VMGVVMRequest>" + + " <MessageHeader>" + + " <transactionid>%1$s</transactionid>" + + " </MessageHeader>" + + " <MessageBody>" + + " <mdn>%2$s</mdn>" + + " <operation>%3$s</operation>" + + " <source>Device</source>" + + " <devicemodel>%4$s</devicemodel>" + + " </MessageBody>" + + "</VMGVVMRequest>"; + + static final String VMG_URL_KEY = "vmg_url"; + + // Self provisioning POST key/values. VVM3 API 2.1.0 12.3 + private static final String SPG_VZW_MDN_PARAM = "VZW_MDN"; + private static final String SPG_VZW_SERVICE_PARAM = "VZW_SERVICE"; + private static final String SPG_VZW_SERVICE_BASIC = "BVVM"; + private static final String SPG_DEVICE_MODEL_PARAM = "DEVICE_MODEL"; + // Value for all android device + private static final String SPG_DEVICE_MODEL_ANDROID = "DROID_4G"; + private static final String SPG_APP_TOKEN_PARAM = "APP_TOKEN"; + private static final String SPG_APP_TOKEN = "q8e3t5u2o1"; + private static final String SPG_LANGUAGE_PARAM = "SPG_LANGUAGE_PARAM"; + private static final String SPG_LANGUAGE_EN = "ENGLISH"; + + private static final String BASIC_SUBSCRIBE_LINK_TEXT = "Subscribe to Basic Visual Voice Mail"; + + private static final int REQUEST_TIMEOUT_SECONDS = 30; + + private final ActivationTask mTask; + private final PhoneAccountHandle mHandle; + private final OmtpVvmCarrierConfigHelper mHelper; + private final VoicemailStatus.Editor mStatus; + private final Bundle mData; + + private final String mNumber; + + private RequestQueue mRequestQueue; + + private static class ProvisioningException extends Exception { + + public ProvisioningException(String message) { + super(message); + } + } + + static { + // Set the default cookie handler to retain session data for the self provisioning gateway. + // Note; this is not ideal as it is application-wide, and can easily get clobbered. + // But it seems to be the preferred way to manage cookie for HttpURLConnection, and manually + // managing cookies will greatly increase complexity. + CookieManager cookieManager = new CookieManager(); + CookieHandler.setDefault(cookieManager); + } + + @WorkerThread + public Vvm3Subscriber(ActivationTask task, PhoneAccountHandle handle, + OmtpVvmCarrierConfigHelper helper, VoicemailStatus.Editor status, Bundle data) { + Assert.isNotMainThread(); + mTask = task; + mHandle = handle; + mHelper = helper; + mStatus = status; + mData = data; + + // Assuming getLine1Number() will work with VVM3. For unprovisioned users the IMAP username + // is not included in the status SMS, thus no other way to get the current phone number. + mNumber = mHelper.getContext().getSystemService(TelephonyManager.class) + .createForPhoneAccountHandle(mHandle).getLine1Number(); + } + + @WorkerThread + public void subscribe() { + Assert.isNotMainThread(); + // Cellular data is required to subscribe. + // processSubscription() is called after network is available. + VvmLog.i(TAG, "Subscribing"); + + try (NetworkWrapper wrapper = VvmNetworkRequest.getNetwork(mHelper, mHandle, mStatus)) { + Network network = wrapper.get(); + VvmLog.d(TAG, "provisioning: network available"); + mRequestQueue = Volley + .newRequestQueue(mHelper.getContext(), new NetworkSpecifiedHurlStack(network)); + processSubscription(); + } catch (RequestFailedException e) { + mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED); + mTask.fail(); + } + } + + private void processSubscription() { + try { + String gatewayUrl = getSelfProvisioningGateway(); + String selfProvisionResponse = getSelfProvisionResponse(gatewayUrl); + String subscribeLink = findSubscribeLink(selfProvisionResponse); + clickSubscribeLink(subscribeLink); + } catch (ProvisioningException e) { + VvmLog.e(TAG, e.toString()); + mTask.fail(); + } + } + + /** + * Get the URL to perform self-provisioning from the voicemail management gateway. + */ + private String getSelfProvisioningGateway() throws ProvisioningException { + VvmLog.i(TAG, "retrieving SPG URL"); + String response = vvm3XmlRequest(OPERATION_GET_SPG_URL); + return extractText(response, SPG_URL_TAG); + } + + /** + * Sent a request to the self-provisioning gateway, which will return us with a webpage. The + * page might contain a "Subscribe to Basic Visual Voice Mail" link to complete the + * subscription. The cookie from this response and cellular data is required to click the link. + */ + private String getSelfProvisionResponse(String url) throws ProvisioningException { + VvmLog.i(TAG, "Retrieving self provisioning response"); + + RequestFuture<String> future = RequestFuture.newFuture(); + + StringRequest stringRequest = new StringRequest(Request.Method.POST, url, future, future) { + @Override + protected Map<String, String> getParams() { + Map<String, String> params = new ArrayMap<>(); + params.put(SPG_VZW_MDN_PARAM, mNumber); + params.put(SPG_VZW_SERVICE_PARAM, SPG_VZW_SERVICE_BASIC); + params.put(SPG_DEVICE_MODEL_PARAM, SPG_DEVICE_MODEL_ANDROID); + params.put(SPG_APP_TOKEN_PARAM, SPG_APP_TOKEN); + // Language to display the subscription page. The page is never shown to the user + // so just use English. + params.put(SPG_LANGUAGE_PARAM, SPG_LANGUAGE_EN); + return params; + } + }; + + mRequestQueue.add(stringRequest); + try { + return future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (InterruptedException | ExecutionException | TimeoutException e) { + mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED); + throw new ProvisioningException(e.toString()); + } + } + + private void clickSubscribeLink(String subscribeLink) throws ProvisioningException { + VvmLog.i(TAG, "Clicking subscribe link"); + RequestFuture<String> future = RequestFuture.newFuture(); + + StringRequest stringRequest = new StringRequest(Request.Method.POST, + subscribeLink, future, future); + mRequestQueue.add(stringRequest); + try { + // A new STATUS SMS will be sent after this request. + future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + } catch (TimeoutException | ExecutionException | InterruptedException e) { + mHelper.handleEvent(mStatus, OmtpEvents.VVM3_SPG_CONNECTION_FAILED); + throw new ProvisioningException(e.toString()); + } + // It could take very long for the STATUS SMS to return. Waiting for it is unreliable. + // Just leave the CONFIG STATUS as CONFIGURING and end the task. The user can always + // manually retry if it took too long. + } + + private String vvm3XmlRequest(String operation) throws ProvisioningException { + VvmLog.d(TAG, "Sending vvm3XmlRequest for " + operation); + String voicemailManagementGateway = mData.getString(VMG_URL_KEY); + if (voicemailManagementGateway == null) { + VvmLog.e(TAG, "voicemailManagementGateway url unknown"); + return null; + } + String transactionId = createTransactionId(); + String body = String.format(Locale.US, VMG_XML_REQUEST_FORMAT, + transactionId, mNumber, operation, Build.MODEL); + + RequestFuture<String> future = RequestFuture.newFuture(); + StringRequest stringRequest = new StringRequest(Request.Method.POST, + voicemailManagementGateway, future, future) { + @Override + public byte[] getBody() throws AuthFailureError { + return body.getBytes(); + } + }; + mRequestQueue.add(stringRequest); + + try { + String response = future.get(REQUEST_TIMEOUT_SECONDS, TimeUnit.SECONDS); + if (!transactionId.equals(extractText(response, TRANSACTION_ID_TAG))) { + throw new ProvisioningException("transactionId mismatch"); + } + return response; + } catch (InterruptedException | ExecutionException | TimeoutException e) { + mHelper.handleEvent(mStatus, OmtpEvents.VVM3_VMG_CONNECTION_FAILED); + throw new ProvisioningException(e.toString()); + } + } + + private String findSubscribeLink(String response) throws ProvisioningException { + Spanned doc = Html.fromHtml(response, Html.FROM_HTML_MODE_LEGACY); + URLSpan[] spans = doc.getSpans(0, doc.length(), URLSpan.class); + StringBuilder fulltext = new StringBuilder(); + for (URLSpan span : spans) { + String text = doc.subSequence(doc.getSpanStart(span), doc.getSpanEnd(span)).toString(); + if (BASIC_SUBSCRIBE_LINK_TEXT.equals(text)) { + return span.getURL(); + } + fulltext.append(text); + } + throw new ProvisioningException("Subscribe link not found: " + fulltext); + } + + private String createTransactionId() { + return String.valueOf(Math.abs(new Random().nextLong())); + } + + private String extractText(String xml, String tag) throws ProvisioningException { + Pattern pattern = Pattern.compile("<" + tag + ">(.*)<\\/" + tag + ">"); + Matcher matcher = pattern.matcher(xml); + if (matcher.find()) { + return matcher.group(1); + } + throw new ProvisioningException("Tag " + tag + " not found in xml response"); + } + + private static class NetworkSpecifiedHurlStack extends HurlStack { + + private final Network mNetwork; + + public NetworkSpecifiedHurlStack(Network network) { + mNetwork = network; + } + + @Override + protected HttpURLConnection createConnection(URL url) throws IOException { + return (HttpURLConnection) mNetwork.openConnection(url); + } + + } +} diff --git a/java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml b/java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml new file mode 100644 index 000000000..b0db64b12 --- /dev/null +++ b/java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml @@ -0,0 +1,97 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* +** Copyright 2014, 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. +*/ +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center_horizontal" + android:orientation="vertical"> + <!-- header text ('Enter Pin') --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:orientation="vertical" + android:paddingTop="48dp" + android:paddingStart="48dp" + android:paddingEnd="48dp"> + <TextView + android:id="@+id/headerText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:lines="2" + android:textAppearance="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle" + android:accessibilityLiveRegion="polite"/> + + <!-- hint text ('PIN too short') --> + <TextView + android:id="@+id/hintText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:lines="2" /> + + <!-- error text ('PIN too short') --> + <TextView + android:id="@+id/errorText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:lines="2" + android:textColor="@android:color/holo_red_dark"/> + + <!-- Password entry field --> + <EditText + android:id="@+id/pin_entry" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center" + android:imeOptions="actionNext|flagNoExtractUi" + android:inputType="numberPassword" + android:textSize="24sp"/> + </LinearLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" + android:gravity="end" + android:orientation="horizontal"> + + <!-- left : cancel --> + <Button + android:id="@+id/cancel_button" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="@string/change_pin_cancel_label"/> + + <!-- right : continue --> + <Button + android:id="@+id/next_button" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:text="@string/change_pin_continue_label"/> + + </LinearLayout> +</LinearLayout> diff --git a/java/com/android/voicemailomtp/res/values/arrays.xml b/java/com/android/voicemailomtp/res/values/arrays.xml new file mode 100644 index 000000000..95714cf4d --- /dev/null +++ b/java/com/android/voicemailomtp/res/values/arrays.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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. +--> +<resources> + +</resources> diff --git a/java/com/android/voicemailomtp/res/values/attrs.xml b/java/com/android/voicemailomtp/res/values/attrs.xml new file mode 100644 index 000000000..d1c7329d5 --- /dev/null +++ b/java/com/android/voicemailomtp/res/values/attrs.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2006 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. +--> + +<resources> + + <attr name="preferenceBackgroundColor" format="color" /> +</resources> diff --git a/java/com/android/voicemailomtp/res/values/colors.xml b/java/com/android/voicemailomtp/res/values/colors.xml new file mode 100644 index 000000000..8a897ab94 --- /dev/null +++ b/java/com/android/voicemailomtp/res/values/colors.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources> + +</resources> diff --git a/java/com/android/voicemailomtp/res/values/config.xml b/java/com/android/voicemailomtp/res/values/config.xml new file mode 100644 index 000000000..2f5603083 --- /dev/null +++ b/java/com/android/voicemailomtp/res/values/config.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2009 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. +--> + +<resources> + +</resources> diff --git a/java/com/android/voicemailomtp/res/values/dimens.xml b/java/com/android/voicemailomtp/res/values/dimens.xml new file mode 100644 index 000000000..e66ca0921 --- /dev/null +++ b/java/com/android/voicemailomtp/res/values/dimens.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2011 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. +--> + +<resources> + +</resources> diff --git a/java/com/android/voicemailomtp/res/values/ids.xml b/java/com/android/voicemailomtp/res/values/ids.xml new file mode 100644 index 000000000..84c685a14 --- /dev/null +++ b/java/com/android/voicemailomtp/res/values/ids.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 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 + --> + +<resources> + +</resources>
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/res/values/strings.xml b/java/com/android/voicemailomtp/res/values/strings.xml new file mode 100644 index 000000000..7a1407371 --- /dev/null +++ b/java/com/android/voicemailomtp/res/values/strings.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> + + <!-- Title of the "Voicemail" settings screen, with a text label identifying which SIM the settings are for. --> + <string translatable="false" name="voicemail_settings_with_label">Voicemail (<xliff:g id="subscriptionlabel" example="Mock Carrier">%s</xliff:g>)</string> + + <!-- Call settings screen, setting option name --> + <string translatable="false" name="voicemail_settings_label">Voicemail</string> + + <!-- DO NOT TRANSLATE. Internal key for a visual voicemail preference. --> + <string translatable="false" name="voicemail_visual_voicemail_key"> + voicemail_visual_voicemail_key + </string> + <!-- DO NOT TRANSLATE. Internal key for a voicemail change pin preference. --> + <string translatable="false" name="voicemail_change_pin_key">voicemail_change_pin_key</string> + + <!-- Visual voicemail on/off title [CHAR LIMIT=40] --> + <string translatable="false" name="voicemail_visual_voicemail_switch_title">Visual Voicemail</string> + + <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] --> + <string translatable="false" name="voicemail_set_pin_dialog_title">Set PIN</string> + <!-- Voicemail change PIN dialog title [CHAR LIMIT=40] --> + <string translatable="false" name="voicemail_change_pin_dialog_title">Change PIN</string> + + <!-- Hint for the old PIN field in the change vociemail PIN dialog --> + <string translatable="false" name="vm_change_pin_old_pin">Old PIN</string> + <!-- Hint for the new PIN field in the change vociemail PIN dialog --> + <string translatable="false" name="vm_change_pin_new_pin">New PIN</string> + + <!-- Message on the dialog when PIN changing is in progress --> + <string translatable="false" name="vm_change_pin_progress_message">Please wait.</string> + <!-- Error message for the voicemail PIN change if the PIN is too short --> + <string translatable="false" name="vm_change_pin_error_too_short">The new PIN is too short.</string> + <!-- Error message for the voicemail PIN change if the PIN is too long --> + <string translatable="false" name="vm_change_pin_error_too_long">The new PIN is too long.</string> + <!-- Error message for the voicemail PIN change if the PIN is too weak --> + <string translatable="false" name="vm_change_pin_error_too_weak">The new PIN is too weak. A strong password should not have continuous sequence or repeated digits.</string> + <!-- Error message for the voicemail PIN change if the old PIN entered doesn't match --> + <string translatable="false" name="vm_change_pin_error_mismatch">The old PIN does not match.</string> + <!-- Error message for the voicemail PIN change if the new PIN contains invalid character --> + <string translatable="false" name="vm_change_pin_error_invalid">The new PIN contains invalid characters.</string> + <!-- Error message for the voicemail PIN change if operation has failed --> + <string translatable="false" name="vm_change_pin_error_system_error">Unable to change PIN</string> + <!-- Message to replace the transcription if a visual voicemail message is not supported--> + <string translatable="false" name="vvm_unsupported_message_format">Unsupported message type, call <xliff:g id="number" example="*86">%s</xliff:g> to listen.</string> + + <!-- The title for the change voicemail PIN activity --> + <string translatable="false" name="change_pin_title">Change Voicemail PIN</string> + <!-- The label for the continue button in change voicemail PIN activity --> + <string translatable="false" name="change_pin_continue_label">Continue</string> + <!-- The label for the cancel button in change voicemail PIN activity --> + <string translatable="false" name="change_pin_cancel_label">Cancel</string> + <!-- The label for the ok button in change voicemail PIN activity --> + <string translatable="false" name="change_pin_ok_label">Ok</string> + <!-- The title for the enter old pin step in change voicemail PIN activity --> + <string translatable="false" name="change_pin_enter_old_pin_header">Confirm your old PIN</string> + <!-- The hint for the enter old pin step in change voicemail PIN activity --> + <string translatable="false" name="change_pin_enter_old_pin_hint">Enter your voicemail PIN to continue.</string> + <!-- The title for the enter new pin step in change voicemail PIN activity --> + <string translatable="false" name="change_pin_enter_new_pin_header">Set a new PIN</string> + <!-- The hint for the enter new pin step in change voicemail PIN activity --> + <string translatable="false" name="change_pin_enter_new_pin_hint">PIN must be <xliff:g id="min" example="4">%1$d</xliff:g>-<xliff:g id="max" example="7">%2$d</xliff:g> digits.</string> + <!-- The title for the confirm new pin step in change voicemail PIN activity --> + <string translatable="false" name="change_pin_confirm_pin_header">Confirm your PIN</string> + <!-- The error message for th confirm new pin step in change voicemail PIN activity, if the pin doen't match the one previously entered --> + <string translatable="false" name="change_pin_confirm_pins_dont_match">PINs don\'t match</string> + <!-- The toast to show after the voicemail PIN has been successfully changed --> + <string translatable="false" name="change_pin_succeeded">Voicemail PIN updated</string> + <!-- The error message to show if the server reported an error while attempting to change the voicemail PIN --> + <string translatable="false" name="change_pin_system_error">Unable to set PIN</string> +</resources> diff --git a/java/com/android/voicemailomtp/res/values/styles.xml b/java/com/android/voicemailomtp/res/values/styles.xml new file mode 100644 index 000000000..8a897ab94 --- /dev/null +++ b/java/com/android/voicemailomtp/res/values/styles.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<resources> + +</resources> diff --git a/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml b/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml new file mode 100644 index 000000000..03bc34efc --- /dev/null +++ b/java/com/android/voicemailomtp/res/xml/voicemail_settings.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2014 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. +--> + +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" + android:title="@string/voicemail_settings_label"> + + <SwitchPreference + android:key="@string/voicemail_visual_voicemail_key" + android:title="@string/voicemail_visual_voicemail_switch_title"/>" + + <Preference + android:key="@string/voicemail_change_pin_key" + android:title="@string/voicemail_change_pin_dialog_title"/> +</PreferenceScreen> diff --git a/java/com/android/voicemailomtp/res/xml/vvm_config.xml b/java/com/android/voicemailomtp/res/xml/vvm_config.xml new file mode 100644 index 000000000..19c667e13 --- /dev/null +++ b/java/com/android/voicemailomtp/res/xml/vvm_config.xml @@ -0,0 +1,134 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2016 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. +--> + +<list name="carrier_config_list"> + <pbundle_as_map> + <!-- Test --> + <string-array name="mccmnc"> + <item value="TEST"/> + </string-array> + </pbundle_as_map> + + <pbundle_as_map> + <!-- Orange France --> + <string-array name="mccmnc"> + <item value="20801"/> + <item value="20802"/> + </string-array> + + <int name="vvm_port_number_int" value="20481"/> + <string name="vvm_destination_number_string">21101</string> + <string-array name="carrier_vvm_package_name_string_array"> + <item value="com.orange.vvm"/> + </string-array> + <string name="vvm_type_string">vvm_type_omtp</string> + <boolean name="vvm_cellular_data_required_bool" value="true"/> + <string-array name="vvm_disabled_capabilities_string_array"> + <!-- b/32365569 --> + <item value="STARTTLS"/> + </string-array> + </pbundle_as_map> + + <pbundle_as_map> + <!-- T-Mobile USA--> + <string-array name="mccmnc"> + <item value="310160"/> + <item value="310200"/> + <item value="310210"/> + <item value="310220"/> + <item value="310230"/> + <item value="310240"/> + <item value="310250"/> + <item value="310260"/> + <item value="310270"/> + <item value="310300"/> + <item value="310310"/> + <item value="310490"/> + <item value="310530"/> + <item value="310590"/> + <item value="310640"/> + <item value="310660"/> + <item value="310800"/> + </string-array> + + <int name="vvm_port_number_int" value="1808"/> + <int name="vvm_ssl_port_number_int" value="993"/> + <string name="vvm_destination_number_string">122</string> + <string-array name="carrier_vvm_package_name_string_array"> + <item value="com.tmobile.vvm.application"/> + </string-array> + <string name="vvm_type_string">vvm_type_cvvm</string>> + <string-array name="vvm_disabled_capabilities_string_array"> + <!-- b/28717550 --> + <item value="AUTH=DIGEST-MD5"/> + </string-array> + </pbundle_as_map> + + <pbundle_as_map> + <!-- Verizon USA --> + <string-array name="mccmnc"> + <item value="310004"/> + <item value="310010"/> + <item value="310012"/> + <item value="310013"/> + <item value="310590"/> + <item value="310890"/> + <item value="310910"/> + <item value="311110"/> + <item value="311270"/> + <item value="311271"/> + <item value="311272"/> + <item value="311273"/> + <item value="311274"/> + <item value="311275"/> + <item value="311276"/> + <item value="311277"/> + <item value="311278"/> + <item value="311279"/> + <item value="311280"/> + <item value="311281"/> + <item value="311282"/> + <item value="311283"/> + <item value="311284"/> + <item value="311285"/> + <item value="311286"/> + <item value="311287"/> + <item value="311288"/> + <item value="311289"/> + <item value="311390"/> + <item value="311480"/> + <item value="311481"/> + <item value="311482"/> + <item value="311483"/> + <item value="311484"/> + <item value="311485"/> + <item value="311486"/> + <item value="311487"/> + <item value="311488"/> + <item value="311489"/> + </string-array> + + <int name="vvm_port_number_int" value="0"/> + <string name="vvm_destination_number_string">900080006200</string> + <string name="vvm_type_string">vvm_type_vvm3</string> + <string name="vvm_client_prefix_string">//VZWVVM</string> + <boolean name="vvm_cellular_data_required_bool" value="true"/> + <boolean name="vvm_legacy_mode_enabled_bool" value="true"/> + <!-- VVM3 specific value for the voicemail management gateway to use if the SMS didn't provide + one --> + <string name="default_vmg_url">https://mobile.vzw.com/VMGIMS/VMServices</string> + </pbundle_as_map> +</list> diff --git a/java/com/android/voicemailomtp/scheduling/BaseTask.java b/java/com/android/voicemailomtp/scheduling/BaseTask.java new file mode 100644 index 000000000..8097bb4dc --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/BaseTask.java @@ -0,0 +1,206 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.scheduling; + +import android.content.Context; +import android.content.Intent; +import android.os.SystemClock; +import android.support.annotation.CallSuper; +import android.support.annotation.MainThread; +import android.support.annotation.NonNull; +import android.support.annotation.WorkerThread; +import android.telecom.PhoneAccountHandle; +import android.telephony.SubscriptionManager; +import com.android.voicemailomtp.Assert; +import com.android.voicemailomtp.NeededForTesting; +import java.util.ArrayList; +import java.util.List; + +/** + * Provides common utilities for task implementations, such as execution time and managing {@link + * Policy} + */ +public abstract class BaseTask implements Task { + + private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle"; + + private Context mContext; + + private int mId; + private PhoneAccountHandle mPhoneAccountHandle; + + private boolean mHasStarted; + private volatile boolean mHasFailed; + + @NonNull + private final List<Policy> mPolicies = new ArrayList<>(); + + private long mExecutionTime; + + private static Clock sClock = new Clock(); + + protected BaseTask(int id) { + mId = id; + mExecutionTime = getTimeMillis(); + } + + /** + * Modify the task ID to prevent arbitrary task from executing. Can only be called before {@link + * #onCreate(Context, Intent, int, int)} returns. + */ + @MainThread + public void setId(int id) { + Assert.isMainThread(); + mId = id; + } + + @MainThread + public boolean hasStarted() { + Assert.isMainThread(); + return mHasStarted; + } + + @MainThread + public boolean hasFailed() { + Assert.isMainThread(); + return mHasFailed; + } + + public Context getContext() { + return mContext; + } + + public PhoneAccountHandle getPhoneAccountHandle() { + return mPhoneAccountHandle; + } + /** + * Should be call in the constructor or {@link Policy#onCreate(BaseTask, Intent, int, int)} will + * be missed. + */ + @MainThread + public BaseTask addPolicy(Policy policy) { + Assert.isMainThread(); + mPolicies.add(policy); + return this; + } + + /** + * Indicate the task has failed. {@link Policy#onFail()} will be triggered once the execution + * ends. This mechanism is used by policies for actions such as determining whether to schedule + * a retry. Must be call inside {@link #onExecuteInBackgroundThread()} + */ + @WorkerThread + public void fail() { + Assert.isNotMainThread(); + mHasFailed = true; + } + + @MainThread + public void setExecutionTime(long timeMillis) { + Assert.isMainThread(); + mExecutionTime = timeMillis; + } + + public long getTimeMillis() { + return sClock.getTimeMillis(); + } + + /** + * Creates an intent that can be used to restart the current task. Derived class should build + * their intent upon this. + */ + public Intent createRestartIntent() { + return createIntent(getContext(), this.getClass(), mPhoneAccountHandle); + } + + /** + * Creates an intent that can be used to start the {@link TaskSchedulerService}. Derived class + * should build their intent upon this. + */ + public static Intent createIntent(Context context, Class<? extends BaseTask> task, + PhoneAccountHandle phoneAccountHandle) { + Intent intent = TaskSchedulerService.createIntent(context, task); + intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phoneAccountHandle); + return intent; + } + + @Override + public TaskId getId() { + return new TaskId(mId, mPhoneAccountHandle); + } + + @Override + @CallSuper + public void onCreate(Context context, Intent intent, int flags, int startId) { + mContext = context; + mPhoneAccountHandle = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE); + for (Policy policy : mPolicies) { + policy.onCreate(this, intent, flags, startId); + } + } + + @Override + public long getReadyInMilliSeconds() { + return mExecutionTime - getTimeMillis(); + } + + @Override + @CallSuper + public void onBeforeExecute() { + for (Policy policy : mPolicies) { + policy.onBeforeExecute(); + } + mHasStarted = true; + } + + @Override + @CallSuper + public void onCompleted() { + if (mHasFailed) { + for (Policy policy : mPolicies) { + policy.onFail(); + } + } + + for (Policy policy : mPolicies) { + policy.onCompleted(); + } + } + + @Override + public void onDuplicatedTaskAdded(Task task) { + for (Policy policy : mPolicies) { + policy.onDuplicatedTaskAdded(); + } + } + + @NeededForTesting + static class Clock { + + public long getTimeMillis() { + return SystemClock.elapsedRealtime(); + } + } + + /** + * Used to replace the clock with an deterministic clock + */ + @NeededForTesting + static void setClockForTesting(Clock clock) { + sClock = clock; + } +} diff --git a/java/com/android/voicemailomtp/scheduling/BlockerTask.java b/java/com/android/voicemailomtp/scheduling/BlockerTask.java new file mode 100644 index 000000000..55ad9a7fd --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/BlockerTask.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.scheduling; + +import android.content.Context; +import android.content.Intent; + +import com.android.voicemailomtp.VvmLog; + +/** + * Task to block another task of the same ID from being queued for a certain amount of time. + */ +public class BlockerTask extends BaseTask { + + private static final String TAG = "BlockerTask"; + + public static final String EXTRA_TASK_ID = "extra_task_id"; + public static final String EXTRA_BLOCK_FOR_MILLIS = "extra_block_for_millis"; + + public BlockerTask() { + super(TASK_INVALID); + } + + @Override + public void onCreate(Context context, Intent intent, int flags, int startId) { + super.onCreate(context, intent, flags, startId); + setId(intent.getIntExtra(EXTRA_TASK_ID, TASK_INVALID)); + setExecutionTime(getTimeMillis() + intent.getIntExtra(EXTRA_BLOCK_FOR_MILLIS, 0)); + } + + @Override + public void onExecuteInBackgroundThread() { + // Do nothing. + } + + @Override + public void onDuplicatedTaskAdded(Task task) { + VvmLog + .v(TAG, task.toString() + "blocked, " + getReadyInMilliSeconds() + "millis remaining"); + } +} diff --git a/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java b/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java new file mode 100644 index 000000000..bef449b30 --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.scheduling; + +import android.content.Intent; + +import com.android.voicemailomtp.scheduling.Task.TaskId; + +/** + * If a task with this policy succeeds, a {@link BlockerTask} with the same {@link TaskId} of the + * task will be queued immediately, preventing the same task from running for a certain amount of + * time. + */ +public class MinimalIntervalPolicy implements Policy { + + BaseTask mTask; + TaskId mId; + int mBlockForMillis; + + public MinimalIntervalPolicy(int blockForMillis) { + mBlockForMillis = blockForMillis; + } + + @Override + public void onCreate(BaseTask task, Intent intent, int flags, int startId) { + mTask = task; + mId = mTask.getId(); + } + + @Override + public void onBeforeExecute() { + + } + + @Override + public void onCompleted() { + if (!mTask.hasFailed()) { + Intent intent = mTask + .createIntent(mTask.getContext(), BlockerTask.class, mId.phoneAccountHandle); + intent.putExtra(BlockerTask.EXTRA_TASK_ID, mId.id); + intent.putExtra(BlockerTask.EXTRA_BLOCK_FOR_MILLIS, mBlockForMillis); + mTask.getContext().startService(intent); + } + } + + @Override + public void onFail() { + + } + + @Override + public void onDuplicatedTaskAdded() { + + } +} diff --git a/java/com/android/voicemailomtp/scheduling/Policy.java b/java/com/android/voicemailomtp/scheduling/Policy.java new file mode 100644 index 000000000..4a475d2ed --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/Policy.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.scheduling; + +import android.content.Intent; + +/** + * A set of listeners managed by {@link BaseTask} for common behaviors such as retrying. Call {@link + * BaseTask#addPolicy(Policy)} to add a policy. + */ +public interface Policy { + + void onCreate(BaseTask task, Intent intent, int flags, int startId); + + void onBeforeExecute(); + + void onCompleted(); + + void onFail(); + + void onDuplicatedTaskAdded(); +} diff --git a/java/com/android/voicemailomtp/scheduling/PostponePolicy.java b/java/com/android/voicemailomtp/scheduling/PostponePolicy.java new file mode 100644 index 000000000..27a82f0ef --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/PostponePolicy.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.scheduling; + +import android.content.Intent; + +import com.android.voicemailomtp.VvmLog; + +/** + * A task with Postpone policy will not be executed immediately. It will wait for a while and if a + * duplicated task is queued during the duration, the task will be postponed further. The task will + * only be executed if no new task was added in postponeMillis. Useful to batch small tasks in quick + * succession together. + */ +public class PostponePolicy implements Policy { + + private static final String TAG = "PostponePolicy"; + + private final int mPostponeMillis; + private BaseTask mTask; + + public PostponePolicy(int postponeMillis) { + mPostponeMillis = postponeMillis; + } + + @Override + public void onCreate(BaseTask task, Intent intent, int flags, int startId) { + mTask = task; + mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis); + } + + @Override + public void onBeforeExecute() { + // Do nothing + } + + @Override + public void onCompleted() { + // Do nothing + } + + @Override + public void onFail() { + // Do nothing + } + + @Override + public void onDuplicatedTaskAdded() { + if (mTask.hasStarted()) { + return; + } + VvmLog.d(TAG, "postponing " + mTask); + mTask.setExecutionTime(mTask.getTimeMillis() + mPostponeMillis); + } +} diff --git a/java/com/android/voicemailomtp/scheduling/RetryPolicy.java b/java/com/android/voicemailomtp/scheduling/RetryPolicy.java new file mode 100644 index 000000000..463657483 --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/RetryPolicy.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.scheduling; + +import android.content.Intent; +import android.telecom.PhoneAccountHandle; + +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; + +/** + * A task with this policy will automatically re-queue itself if {@link BaseTask#fail()} has been + * called during {@link BaseTask#onExecuteInBackgroundThread()}. A task will be retried at most + * <code>retryLimit</code> times and with a <code>retryDelayMillis</code> interval in between. + */ +public class RetryPolicy implements Policy { + + private static final String TAG = "RetryPolicy"; + private static final String EXTRA_RETRY_COUNT = "extra_retry_count"; + + private final int mRetryLimit; + private final int mRetryDelayMillis; + + private BaseTask mTask; + + private int mRetryCount; + private boolean mFailed; + + private VoicemailStatus.DeferredEditor mVoicemailStatusEditor; + + public RetryPolicy(int retryLimit, int retryDelayMillis) { + mRetryLimit = retryLimit; + mRetryDelayMillis = retryDelayMillis; + } + + private boolean hasMoreRetries() { + return mRetryCount < mRetryLimit; + } + + /** + * Error status should only be set if retries has exhausted or the task is successful. Status + * writes to this editor will be deferred until the task has ended, and will only be committed + * if the task is successful or there are no retries left. + */ + public VoicemailStatus.Editor getVoicemailStatusEditor() { + return mVoicemailStatusEditor; + } + + @Override + public void onCreate(BaseTask task, Intent intent, int flags, int startId) { + mTask = task; + mRetryCount = intent.getIntExtra(EXTRA_RETRY_COUNT, 0); + if (mRetryCount > 0) { + VvmLog.d(TAG, "retry #" + mRetryCount + " for " + mTask + " queued, executing in " + + mRetryDelayMillis); + mTask.setExecutionTime(mTask.getTimeMillis() + mRetryDelayMillis); + } + PhoneAccountHandle phoneAccountHandle = task.getPhoneAccountHandle(); + if (phoneAccountHandle == null) { + VvmLog.e(TAG, + "null phone account for phoneAccountHandle " + task.getPhoneAccountHandle()); + // This should never happen, but continue on if it does. The status write will be + // discarded. + } + mVoicemailStatusEditor = VoicemailStatus + .deferredEdit(task.getContext(), phoneAccountHandle); + } + + @Override + public void onBeforeExecute() { + + } + + @Override + public void onCompleted() { + if (!mFailed || !hasMoreRetries()) { + if (!mFailed) { + VvmLog.d(TAG, mTask.toString() + " completed successfully"); + } + if (!hasMoreRetries()) { + VvmLog.d(TAG, "Retry limit for " + mTask + " reached"); + } + VvmLog.i(TAG, "committing deferred status: " + mVoicemailStatusEditor.getValues()); + mVoicemailStatusEditor.deferredApply(); + return; + } + VvmLog.i(TAG, "discarding deferred status: " + mVoicemailStatusEditor.getValues()); + Intent intent = mTask.createRestartIntent(); + intent.putExtra(EXTRA_RETRY_COUNT, mRetryCount + 1); + + mTask.getContext().startService(intent); + } + + @Override + public void onFail() { + mFailed = true; + } + + @Override + public void onDuplicatedTaskAdded() { + + } +} diff --git a/java/com/android/voicemailomtp/scheduling/Task.java b/java/com/android/voicemailomtp/scheduling/Task.java new file mode 100644 index 000000000..61c35396b --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/Task.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.scheduling; + +import android.content.Context; +import android.content.Intent; +import android.support.annotation.MainThread; +import android.support.annotation.WorkerThread; +import android.telecom.PhoneAccountHandle; + +import java.util.Objects; + +/** + * A task for {@link TaskSchedulerService} to execute. Since the task is sent through a intent to + * the scheduler, The task must be constructable with the intent. Specifically, It must have a + * constructor with zero arguments, and have all relevant data packed inside the intent. Use {@link + * TaskSchedulerService#createIntent(Context, Class)} to create a intent that will construct the + * Task. + * + * <p>Only {@link #onExecuteInBackgroundThread()} is run on the worker thread. + */ +public interface Task { + + /** + * TaskId to indicate it has not be set. If a task does not provide a default TaskId it should + * be set before {@link Task#onCreate(Context, Intent, int, int) returns} + */ + int TASK_INVALID = -1; + + /** + * TaskId to indicate it should always be queued regardless of duplicates. {@link + * Task#onDuplicatedTaskAdded(Task)} will never be called on tasks with this TaskId. + */ + int TASK_ALLOW_DUPLICATES = -2; + + int TASK_UPLOAD = 1; + int TASK_SYNC = 2; + int TASK_ACTIVATION = 3; + + /** + * Used to differentiate between types of tasks. If a task with the same TaskId is already in + * the queue the new task will be rejected. + */ + class TaskId { + + /** + * Indicates the operation type of the task. + */ + public final int id; + /** + * Same operation for a different phoneAccountHandle is allowed. phoneAccountHandle is used + * to differentiate phone accounts in multi-SIM scenario. For example, each SIM can queue a + * sync task for their own. + */ + public final PhoneAccountHandle phoneAccountHandle; + + public TaskId(int id, PhoneAccountHandle phoneAccountHandle) { + this.id = id; + this.phoneAccountHandle = phoneAccountHandle; + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof TaskId)) { + return false; + } + TaskId other = (TaskId) object; + return id == other.id && phoneAccountHandle.equals(other.phoneAccountHandle); + } + + @Override + public int hashCode() { + return Objects.hash(id, phoneAccountHandle); + } + } + + TaskId getId(); + + @MainThread + void onCreate(Context context, Intent intent, int flags, int startId); + + /** + * @return number of milliSeconds the scheduler should wait before running this task. A value + * less than {@link TaskSchedulerService#READY_TOLERANCE_MILLISECONDS} will be considered ready. + * If no tasks are ready, the scheduler will sleep for this amount of time before doing another + * check (it will still wake if a new task is added). The first task in the queue that is ready + * will be executed. + */ + @MainThread + long getReadyInMilliSeconds(); + + /** + * Called on the main thread when the scheduler is about to send the task into the worker + * thread, calling {@link #onExecuteInBackgroundThread()} + */ + @MainThread + void onBeforeExecute(); + + /** + * The actual payload of the task, executed on the worker thread. + */ + @WorkerThread + void onExecuteInBackgroundThread(); + + /** + * Called on the main thread when {@link #onExecuteInBackgroundThread()} has finished or thrown + * an uncaught exception. The task is already removed from the queue at this point, and a same + * task can be queued again. + */ + @MainThread + void onCompleted(); + + /** + * Another task with the same TaskId has been added. Necessary data can be retrieved from the + * other task, and after this returns the task will be discarded. + */ + @MainThread + void onDuplicatedTaskAdded(Task task); +} diff --git a/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java b/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java new file mode 100644 index 000000000..90b50e913 --- /dev/null +++ b/java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java @@ -0,0 +1,392 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.scheduling; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.app.Service; +import android.content.Context; +import android.content.Intent; +import android.os.Binder; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.os.Looper; +import android.os.Message; +import android.os.PowerManager; +import android.os.PowerManager.WakeLock; +import android.os.SystemClock; +import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; +import android.support.annotation.WorkerThread; +import com.android.voicemailomtp.Assert; +import com.android.voicemailomtp.NeededForTesting; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.scheduling.Task.TaskId; +import java.util.ArrayDeque; +import java.util.Queue; + +/** + * A service to queue and run {@link Task} on a worker thread. Only one task will be ran at a time, + * and same task cannot exist in the queue at the same time. The service will be started when a + * intent is received, and stopped when there are no more tasks in the queue. + */ +public class TaskSchedulerService extends Service { + + private static final String TAG = "VvmTaskScheduler"; + + private static final String ACTION_WAKEUP = "action_wakeup"; + + private static final int READY_TOLERANCE_MILLISECONDS = 100; + + /** + * Threshold to determine whether to do a short or long sleep when a task is scheduled in the + * future. + * + * <p>A short sleep will continue to held the wake lock and use {@link + * Handler#postDelayed(Runnable, long)} to wait for the next task. + * + * <p>A long sleep will release the wake lock and set a {@link AlarmManager} alarm. The alarm is + * exact and will wake up the device. Note: as this service is run in the telephony process it + * does not seem to be restricted by doze or sleep, it will fire exactly at the moment. The + * unbundled version should take doze into account. + */ + private static final int SHORT_SLEEP_THRESHOLD_MILLISECONDS = 60_000; + /** + * When there are no more tasks to be run the service should be stopped. But when all tasks has + * finished there might still be more tasks in the message queue waiting to be processed, + * especially the ones submitted in {@link Task#onCompleted()}. Wait for a while before stopping + * the service to make sure there are no pending messages. + */ + private static final int STOP_DELAY_MILLISECONDS = 5_000; + private static final String EXTRA_CLASS_NAME = "extra_class_name"; + + private static final String WAKE_LOCK_TAG = "TaskSchedulerService_wakelock"; + + // The thread to run tasks on + private volatile WorkerThreadHandler mWorkerThreadHandler; + + private Context mContext = this; + /** + * Used by tests to turn task handling into a single threaded process by calling {@link + * Handler#handleMessage(Message)} directly + */ + private MessageSender mMessageSender = new MessageSender(); + + private MainThreadHandler mMainThreadHandler; + + private WakeLock mWakeLock; + + /** + * Main thread only, access through {@link #getTasks()} + */ + private final Queue<Task> mTasks = new ArrayDeque<>(); + private boolean mWorkerThreadIsBusy = false; + + private final Runnable mStopServiceWithDelay = new Runnable() { + @Override + public void run() { + VvmLog.d(TAG, "Stopping service"); + stopSelf(); + } + }; + /** + * Should attempt to run the next task when a task has finished or been added. + */ + private boolean mTaskAutoRunDisabledForTesting = false; + + @VisibleForTesting + final class WorkerThreadHandler extends Handler { + + public WorkerThreadHandler(Looper looper) { + super(looper); + } + + @Override + @WorkerThread + public void handleMessage(Message msg) { + Assert.isNotMainThread(); + Task task = (Task) msg.obj; + try { + VvmLog.v(TAG, "executing task " + task); + task.onExecuteInBackgroundThread(); + } catch (Throwable throwable) { + VvmLog.e(TAG, "Exception while executing task " + task + ":", throwable); + } + + Message schedulerMessage = mMainThreadHandler.obtainMessage(); + schedulerMessage.obj = task; + mMessageSender.send(schedulerMessage); + } + } + + @VisibleForTesting + final class MainThreadHandler extends Handler { + + public MainThreadHandler(Looper looper) { + super(looper); + } + + @Override + @MainThread + public void handleMessage(Message msg) { + Assert.isMainThread(); + Task task = (Task) msg.obj; + getTasks().remove(task); + task.onCompleted(); + mWorkerThreadIsBusy = false; + maybeRunNextTask(); + } + } + + @Override + @MainThread + public void onCreate() { + super.onCreate(); + mWakeLock = getSystemService(PowerManager.class) + .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG); + mWakeLock.setReferenceCounted(false); + HandlerThread thread = new HandlerThread("VvmTaskSchedulerService"); + thread.start(); + + mWorkerThreadHandler = new WorkerThreadHandler(thread.getLooper()); + mMainThreadHandler = new MainThreadHandler(Looper.getMainLooper()); + } + + @Override + public void onDestroy() { + mWorkerThreadHandler.getLooper().quit(); + mWakeLock.release(); + } + + @Override + @MainThread + public int onStartCommand(@Nullable Intent intent, int flags, int startId) { + Assert.isMainThread(); + // maybeRunNextTask() will release the wakelock either by entering a long sleep or stopping + // the service. + mWakeLock.acquire(); + if (ACTION_WAKEUP.equals(intent.getAction())) { + VvmLog.d(TAG, "woke up by AlarmManager"); + } else { + Task task = createTask(intent, flags, startId); + if (task == null) { + VvmLog.e(TAG, "cannot create task form intent"); + } else { + addTask(task); + } + } + maybeRunNextTask(); + // STICKY means the service will be automatically restarted will the last intent if it is + // killed. + return START_NOT_STICKY; + } + + @MainThread + @VisibleForTesting + void addTask(Task task) { + Assert.isMainThread(); + if (task.getId().id == Task.TASK_INVALID) { + throw new AssertionError("Task id was not set to a valid value before adding."); + } + if (task.getId().id != Task.TASK_ALLOW_DUPLICATES) { + Task oldTask = getTask(task.getId()); + if (oldTask != null) { + oldTask.onDuplicatedTaskAdded(task); + return; + } + } + mMainThreadHandler.removeCallbacks(mStopServiceWithDelay); + getTasks().add(task); + maybeRunNextTask(); + } + + @MainThread + @Nullable + private Task getTask(TaskId taskId) { + Assert.isMainThread(); + for (Task task : getTasks()) { + if (task.getId().equals(taskId)) { + return task; + } + } + return null; + } + + @MainThread + private Queue<Task> getTasks() { + Assert.isMainThread(); + return mTasks; + } + + /** + * Create an intent that will queue the <code>task</code> + */ + public static Intent createIntent(Context context, Class<? extends Task> task) { + Intent intent = new Intent(context, TaskSchedulerService.class); + intent.putExtra(EXTRA_CLASS_NAME, task.getName()); + return intent; + } + + @VisibleForTesting + @MainThread + @Nullable + Task createTask(@Nullable Intent intent, int flags, int startId) { + Assert.isMainThread(); + if (intent == null) { + return null; + } + String className = intent.getStringExtra(EXTRA_CLASS_NAME); + VvmLog.d(TAG, "create task:" + className); + if (className == null) { + throw new IllegalArgumentException("EXTRA_CLASS_NAME expected"); + } + try { + Task task = (Task) Class.forName(className).newInstance(); + task.onCreate(mContext, intent, flags, startId); + return task; + } catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) { + throw new IllegalArgumentException(e); + } + } + + @MainThread + private void maybeRunNextTask() { + Assert.isMainThread(); + if (mWorkerThreadIsBusy) { + return; + } + if (mTaskAutoRunDisabledForTesting) { + // If mTaskAutoRunDisabledForTesting is true, runNextTask() must be explicitly called + // to run the next task. + return; + } + + runNextTask(); + } + + @VisibleForTesting + @MainThread + void runNextTask() { + Assert.isMainThread(); + // The current alarm is no longer valid, a new one will be set up if required. + getSystemService(AlarmManager.class).cancel(getWakeupIntent()); + if (getTasks().isEmpty()) { + prepareStop(); + return; + } + Long minimalWaitTime = null; + for (Task task : getTasks()) { + long waitTime = task.getReadyInMilliSeconds(); + if (waitTime < READY_TOLERANCE_MILLISECONDS) { + task.onBeforeExecute(); + Message message = mWorkerThreadHandler.obtainMessage(); + message.obj = task; + mWorkerThreadIsBusy = true; + mMessageSender.send(message); + return; + } else { + if (minimalWaitTime == null || waitTime < minimalWaitTime) { + minimalWaitTime = waitTime; + } + } + } + VvmLog.d(TAG, "minimal wait time:" + minimalWaitTime); + if (!mTaskAutoRunDisabledForTesting && minimalWaitTime != null) { + // No tasks are currently ready. Sleep until the next one should be. + // If a new task is added during the sleep the service will wake immediately. + sleep(minimalWaitTime); + } + } + + private void sleep(long timeMillis) { + if (timeMillis < SHORT_SLEEP_THRESHOLD_MILLISECONDS) { + mMainThreadHandler.postDelayed(new Runnable() { + @Override + public void run() { + maybeRunNextTask(); + } + }, timeMillis); + return; + } + + // Tasks does not have a strict timing requirement, use AlarmManager.set() so the OS could + // optimize the battery usage. As this service currently run in the telephony process the + // OS give it privileges to behave the same as setExact(), but set() is the targeted + // behavior once this is unbundled. + getSystemService(AlarmManager.class).set(AlarmManager.ELAPSED_REALTIME_WAKEUP, + SystemClock.elapsedRealtime() + timeMillis, + getWakeupIntent()); + mWakeLock.release(); + VvmLog.d(TAG, "Long sleep for " + timeMillis + " millis"); + } + + private PendingIntent getWakeupIntent() { + Intent intent = new Intent(ACTION_WAKEUP, null, this, getClass()); + return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); + } + + private void prepareStop() { + VvmLog.d(TAG, + "No more tasks, stopping service if no task are added in " + + STOP_DELAY_MILLISECONDS + " millis"); + mMainThreadHandler.postDelayed(mStopServiceWithDelay, STOP_DELAY_MILLISECONDS); + } + + static class MessageSender { + + public void send(Message message) { + message.sendToTarget(); + } + } + + @NeededForTesting + void setContextForTest(Context context) { + mContext = context; + } + + @NeededForTesting + void setTaskAutoRunDisabledForTest(boolean value) { + mTaskAutoRunDisabledForTesting = value; + } + + @NeededForTesting + void setMessageSenderForTest(MessageSender sender) { + mMessageSender = sender; + } + + @NeededForTesting + void clearTasksForTest() { + mTasks.clear(); + } + + @Override + @Nullable + public IBinder onBind(Intent intent) { + return new LocalBinder(); + } + + @NeededForTesting + class LocalBinder extends Binder { + + @NeededForTesting + public TaskSchedulerService getService() { + return TaskSchedulerService.this; + } + } +} diff --git a/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java b/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java new file mode 100644 index 000000000..5cec52842 --- /dev/null +++ b/java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.settings; + +import android.content.Context; +import android.telecom.PhoneAccountHandle; + +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VisualVoicemailPreferences; +import com.android.voicemailomtp.sync.OmtpVvmSourceManager; + +/** + * Save whether or not a particular account is enabled in shared to be retrieved later. + */ +public class VisualVoicemailSettingsUtil { + + private static final String IS_ENABLED_KEY = "is_enabled"; + + + public static void setEnabled(Context context, PhoneAccountHandle phoneAccount, + boolean isEnabled) { + new VisualVoicemailPreferences(context, phoneAccount).edit() + .putBoolean(IS_ENABLED_KEY, isEnabled) + .apply(); + OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, phoneAccount); + if (isEnabled) { + OmtpVvmSourceManager.getInstance(context).addPhoneStateListener(phoneAccount); + config.startActivation(); + } else { + OmtpVvmSourceManager.getInstance(context).removeSource(phoneAccount); + config.startDeactivation(); + } + } + + public static boolean isEnabled(Context context, + PhoneAccountHandle phoneAccount) { + if (phoneAccount == null) { + return false; + } + + VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount); + if (prefs.contains(IS_ENABLED_KEY)) { + // isEnableByDefault is a bit expensive, so don't use it as default value of + // getBoolean(). The "false" here should never be actually used. + return prefs.getBoolean(IS_ENABLED_KEY, false); + } + return new OmtpVvmCarrierConfigHelper(context, phoneAccount).isEnabledByDefault(); + } + + /** + * Whether the client enabled status is explicitly set by user or by default(Whether carrier VVM + * app is installed). This is used to determine whether to disable the client when the carrier + * VVM app is installed. If the carrier VVM app is installed the client should give priority to + * it if the settings are not touched. + */ + public static boolean isEnabledUserSet(Context context, + PhoneAccountHandle phoneAccount) { + if (phoneAccount == null) { + return false; + } + VisualVoicemailPreferences prefs = new VisualVoicemailPreferences(context, phoneAccount); + return prefs.contains(IS_ENABLED_KEY); + } +} diff --git a/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java b/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java new file mode 100644 index 000000000..e679e9970 --- /dev/null +++ b/java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java @@ -0,0 +1,634 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.settings; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.AlertDialog; +import android.app.ProgressDialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.DialogInterface.OnDismissListener; +import android.net.Network; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.Handler; +import android.os.Message; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.text.Editable; +import android.text.InputFilter; +import android.text.InputFilter.LengthFilter; +import android.text.TextWatcher; +import android.view.KeyEvent; +import android.view.MenuItem; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.WindowManager; +import android.view.inputmethod.EditorInfo; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.TextView.OnEditorActionListener; +import android.widget.Toast; +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.OmtpConstants.ChangePinResult; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.R; +import com.android.voicemailomtp.VisualVoicemailPreferences; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.imap.ImapHelper; +import com.android.voicemailomtp.imap.ImapHelper.InitializingException; +import com.android.voicemailomtp.mail.MessagingException; +import com.android.voicemailomtp.sync.VvmNetworkRequestCallback; + +/** + * Dialog to change the voicemail PIN. The TUI (Telephony User Interface) PIN is used when accessing + * traditional voicemail through phone call. The intent to launch this activity must contain {@link + * #EXTRA_PHONE_ACCOUNT_HANDLE} + */ +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class VoicemailChangePinActivity extends Activity + implements OnClickListener, OnEditorActionListener, TextWatcher { + + private static final String TAG = "VmChangePinActivity"; + + public static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle"; + + private static final String KEY_DEFAULT_OLD_PIN = "default_old_pin"; + + private static final int MESSAGE_HANDLE_RESULT = 1; + + private PhoneAccountHandle mPhoneAccountHandle; + private OmtpVvmCarrierConfigHelper mConfig; + + private int mPinMinLength; + private int mPinMaxLength; + + private State mUiState = State.Initial; + private String mOldPin; + private String mFirstPin; + + private ProgressDialog mProgressDialog; + + private TextView mHeaderText; + private TextView mHintText; + private TextView mErrorText; + private EditText mPinEntry; + private Button mCancelButton; + private Button mNextButton; + + private Handler mHandler = new Handler() { + @Override + public void handleMessage(Message message) { + if (message.what == MESSAGE_HANDLE_RESULT) { + mUiState.handleResult(VoicemailChangePinActivity.this, message.arg1); + } + } + }; + + private enum State { + /** + * Empty state to handle initial state transition. Will immediately switch into {@link + * #VerifyOldPin} if a default PIN has been set by the OMTP client, or {@link #EnterOldPin} + * if not. + */ + Initial, + /** + * Prompt the user to enter old PIN. The PIN will be verified with the server before + * proceeding to {@link #EnterNewPin}. + */ + EnterOldPin { + @Override + public void onEnter(VoicemailChangePinActivity activity) { + activity.setHeader(R.string.change_pin_enter_old_pin_header); + activity.mHintText.setText(R.string.change_pin_enter_old_pin_hint); + activity.mNextButton.setText(R.string.change_pin_continue_label); + activity.mErrorText.setText(null); + } + + @Override + public void onInputChanged(VoicemailChangePinActivity activity) { + activity.setNextEnabled(activity.getCurrentPasswordInput().length() > 0); + } + + + @Override + public void handleNext(VoicemailChangePinActivity activity) { + activity.mOldPin = activity.getCurrentPasswordInput(); + activity.verifyOldPin(); + } + + @Override + public void handleResult(VoicemailChangePinActivity activity, + @ChangePinResult int result) { + if (result == OmtpConstants.CHANGE_PIN_SUCCESS) { + activity.updateState(State.EnterNewPin); + } else { + CharSequence message = activity.getChangePinResultMessage(result); + activity.showError(message); + activity.mPinEntry.setText(""); + } + } + }, + /** + * The default old PIN is found. Show a blank screen while verifying with the server to make + * sure the PIN is still valid. If the PIN is still valid, proceed to {@link #EnterNewPin}. + * If not, the user probably changed the PIN through other means, proceed to {@link + * #EnterOldPin}. If any other issue caused the verifying to fail, show an error and exit. + */ + VerifyOldPin { + @Override + public void onEnter(VoicemailChangePinActivity activity) { + activity.findViewById(android.R.id.content).setVisibility(View.INVISIBLE); + activity.verifyOldPin(); + } + + @Override + public void handleResult(final VoicemailChangePinActivity activity, + @ChangePinResult int result) { + if (result == OmtpConstants.CHANGE_PIN_SUCCESS) { + activity.updateState(State.EnterNewPin); + } else if (result == OmtpConstants.CHANGE_PIN_SYSTEM_ERROR) { + activity.getWindow().setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_HIDDEN); + activity.showError(activity.getString(R.string.change_pin_system_error), + new OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + activity.finish(); + } + }); + } else { + VvmLog.e(TAG, "invalid default old PIN: " + activity + .getChangePinResultMessage(result)); + // If the default old PIN is rejected by the server, the PIN is probably changed + // through other means, or the generated pin is invalid + // Wipe the default old PIN so the old PIN input box will be shown to the user + // on the next time. + setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null); + activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET); + activity.updateState(State.EnterOldPin); + } + } + + @Override + public void onLeave(VoicemailChangePinActivity activity) { + activity.findViewById(android.R.id.content).setVisibility(View.VISIBLE); + } + }, + /** + * Let the user enter the new PIN and validate the format. Only length is enforced, PIN + * strength check relies on the server. After a valid PIN is entered, proceed to {@link + * #ConfirmNewPin} + */ + EnterNewPin { + @Override + public void onEnter(VoicemailChangePinActivity activity) { + activity.mHeaderText.setText(R.string.change_pin_enter_new_pin_header); + activity.mNextButton.setText(R.string.change_pin_continue_label); + activity.mHintText.setText( + activity.getString(R.string.change_pin_enter_new_pin_hint, + activity.mPinMinLength, activity.mPinMaxLength)); + } + + @Override + public void onInputChanged(VoicemailChangePinActivity activity) { + String password = activity.getCurrentPasswordInput(); + if (password.length() == 0) { + activity.setNextEnabled(false); + return; + } + CharSequence error = activity.validatePassword(password); + if (error != null) { + activity.mErrorText.setText(error); + activity.setNextEnabled(false); + } else { + activity.mErrorText.setText(null); + activity.setNextEnabled(true); + } + } + + @Override + public void handleNext(VoicemailChangePinActivity activity) { + CharSequence errorMsg; + errorMsg = activity.validatePassword(activity.getCurrentPasswordInput()); + if (errorMsg != null) { + activity.showError(errorMsg); + return; + } + activity.mFirstPin = activity.getCurrentPasswordInput(); + activity.updateState(State.ConfirmNewPin); + } + }, + /** + * Let the user type in the same PIN again to avoid typos. If the PIN matches then perform a + * PIN change to the server. Finish the activity if succeeded. Return to {@link + * #EnterOldPin} if the old PIN is rejected, {@link #EnterNewPin} for other failure. + */ + ConfirmNewPin { + @Override + public void onEnter(VoicemailChangePinActivity activity) { + activity.mHeaderText.setText(R.string.change_pin_confirm_pin_header); + activity.mHintText.setText(null); + activity.mNextButton.setText(R.string.change_pin_ok_label); + } + + @Override + public void onInputChanged(VoicemailChangePinActivity activity) { + if (activity.getCurrentPasswordInput().length() == 0) { + activity.setNextEnabled(false); + return; + } + if (activity.getCurrentPasswordInput().equals(activity.mFirstPin)) { + activity.setNextEnabled(true); + activity.mErrorText.setText(null); + } else { + activity.setNextEnabled(false); + activity.mErrorText.setText(R.string.change_pin_confirm_pins_dont_match); + } + } + + @Override + public void handleResult(VoicemailChangePinActivity activity, + @ChangePinResult int result) { + if (result == OmtpConstants.CHANGE_PIN_SUCCESS) { + // If the PIN change succeeded we no longer know what the old (current) PIN is. + // Wipe the default old PIN so the old PIN input box will be shown to the user + // on the next time. + setDefaultOldPIN(activity, activity.mPhoneAccountHandle, null); + activity.handleOmtpEvent(OmtpEvents.CONFIG_PIN_SET); + + activity.finish(); + + Toast.makeText(activity, activity.getString(R.string.change_pin_succeeded), + Toast.LENGTH_SHORT).show(); + } else { + CharSequence message = activity.getChangePinResultMessage(result); + VvmLog.i(TAG, "Change PIN failed: " + message); + activity.showError(message); + if (result == OmtpConstants.CHANGE_PIN_MISMATCH) { + // Somehow the PIN has changed, prompt to enter the old PIN again. + activity.updateState(State.EnterOldPin); + } else { + // The new PIN failed to fulfil other restrictions imposed by the server. + activity.updateState(State.EnterNewPin); + } + + } + + } + + @Override + public void handleNext(VoicemailChangePinActivity activity) { + activity.processPinChange(activity.mOldPin, activity.mFirstPin); + } + }; + + /** + * The activity has switched from another state to this one. + */ + public void onEnter(VoicemailChangePinActivity activity) { + // Do nothing + } + + /** + * The user has typed something into the PIN input field. Also called after {@link + * #onEnter(VoicemailChangePinActivity)} + */ + public void onInputChanged(VoicemailChangePinActivity activity) { + // Do nothing + } + + /** + * The asynchronous call to change the PIN on the server has returned. + */ + public void handleResult(VoicemailChangePinActivity activity, @ChangePinResult int result) { + // Do nothing + } + + /** + * The user has pressed the "next" button. + */ + public void handleNext(VoicemailChangePinActivity activity) { + // Do nothing + } + + /** + * The activity has switched from this state to another one. + */ + public void onLeave(VoicemailChangePinActivity activity) { + // Do nothing + } + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + mPhoneAccountHandle = getIntent().getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE); + mConfig = new OmtpVvmCarrierConfigHelper(this, mPhoneAccountHandle); + setContentView(R.layout.voicemail_change_pin); + setTitle(R.string.change_pin_title); + + readPinLength(); + + View view = findViewById(android.R.id.content); + + mCancelButton = (Button) view.findViewById(R.id.cancel_button); + mCancelButton.setOnClickListener(this); + mNextButton = (Button) view.findViewById(R.id.next_button); + mNextButton.setOnClickListener(this); + + mPinEntry = (EditText) view.findViewById(R.id.pin_entry); + mPinEntry.setOnEditorActionListener(this); + mPinEntry.addTextChangedListener(this); + if (mPinMaxLength != 0) { + mPinEntry.setFilters(new InputFilter[]{new LengthFilter(mPinMaxLength)}); + } + + + mHeaderText = (TextView) view.findViewById(R.id.headerText); + mHintText = (TextView) view.findViewById(R.id.hintText); + mErrorText = (TextView) view.findViewById(R.id.errorText); + + if (isDefaultOldPinSet(this, mPhoneAccountHandle)) { + mOldPin = getDefaultOldPin(this, mPhoneAccountHandle); + updateState(State.VerifyOldPin); + } else { + updateState(State.EnterOldPin); + } + } + + private void handleOmtpEvent(OmtpEvents event) { + mConfig.handleEvent(getVoicemailStatusEditor(), event); + } + + private VoicemailStatus.Editor getVoicemailStatusEditor() { + // This activity does not have any automatic retry mechanism, errors should be written right + // away. + return VoicemailStatus.edit(this, mPhoneAccountHandle); + } + + /** + * Extracts the pin length requirement sent by the server with a STATUS SMS. + */ + private void readPinLength() { + VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(this, + mPhoneAccountHandle); + // The OMTP pin length format is {min}-{max} + String[] lengths = preferences.getString(OmtpConstants.TUI_PASSWORD_LENGTH, "").split("-"); + if (lengths.length == 2) { + try { + mPinMinLength = Integer.parseInt(lengths[0]); + mPinMaxLength = Integer.parseInt(lengths[1]); + } catch (NumberFormatException e) { + mPinMinLength = 0; + mPinMaxLength = 0; + } + } else { + mPinMinLength = 0; + mPinMaxLength = 0; + } + } + + @Override + public void onResume() { + super.onResume(); + updateState(mUiState); + + } + + public void handleNext() { + if (mPinEntry.length() == 0) { + return; + } + mUiState.handleNext(this); + } + + public void onClick(View v) { + if (v.getId() == R.id.next_button) { + handleNext(); + } else if (v.getId() == R.id.cancel_button) { + finish(); + } + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (!mNextButton.isEnabled()) { + return true; + } + // Check if this was the result of hitting the enter or "done" key + if (actionId == EditorInfo.IME_NULL + || actionId == EditorInfo.IME_ACTION_DONE + || actionId == EditorInfo.IME_ACTION_NEXT) { + handleNext(); + return true; + } + return false; + } + + public void afterTextChanged(Editable s) { + mUiState.onInputChanged(this); + } + + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // Do nothing + } + + public void onTextChanged(CharSequence s, int start, int before, int count) { + // Do nothing + } + + /** + * After replacing the default PIN with a random PIN, call this to store the random PIN. The + * stored PIN will be automatically entered when the user attempts to change the PIN. + */ + public static void setDefaultOldPIN(Context context, PhoneAccountHandle phoneAccountHandle, + String pin) { + new VisualVoicemailPreferences(context, phoneAccountHandle) + .edit().putString(KEY_DEFAULT_OLD_PIN, pin).apply(); + } + + public static boolean isDefaultOldPinSet(Context context, + PhoneAccountHandle phoneAccountHandle) { + return getDefaultOldPin(context, phoneAccountHandle) != null; + } + + private static String getDefaultOldPin(Context context, PhoneAccountHandle phoneAccountHandle) { + return new VisualVoicemailPreferences(context, phoneAccountHandle) + .getString(KEY_DEFAULT_OLD_PIN); + } + + private String getCurrentPasswordInput() { + return mPinEntry.getText().toString(); + } + + private void updateState(State state) { + State previousState = mUiState; + mUiState = state; + if (previousState != state) { + previousState.onLeave(this); + mPinEntry.setText(""); + mUiState.onEnter(this); + } + mUiState.onInputChanged(this); + } + + /** + * Validates PIN and returns a message to display if PIN fails test. + * + * @param password the raw password the user typed in + * @return error message to show to user or null if password is OK + */ + private CharSequence validatePassword(String password) { + if (mPinMinLength == 0 && mPinMaxLength == 0) { + // Invalid length requirement is sent by the server, just accept anything and let the + // server decide. + return null; + } + + if (password.length() < mPinMinLength) { + return getString(R.string.vm_change_pin_error_too_short); + } + return null; + } + + private void setHeader(int text) { + mHeaderText.setText(text); + mPinEntry.setContentDescription(mHeaderText.getText()); + } + + /** + * Get the corresponding message for the {@link ChangePinResult}.<code>result</code> must not + * {@link OmtpConstants#CHANGE_PIN_SUCCESS} + */ + private CharSequence getChangePinResultMessage(@ChangePinResult int result) { + switch (result) { + case OmtpConstants.CHANGE_PIN_TOO_SHORT: + return getString(R.string.vm_change_pin_error_too_short); + case OmtpConstants.CHANGE_PIN_TOO_LONG: + return getString(R.string.vm_change_pin_error_too_long); + case OmtpConstants.CHANGE_PIN_TOO_WEAK: + return getString(R.string.vm_change_pin_error_too_weak); + case OmtpConstants.CHANGE_PIN_INVALID_CHARACTER: + return getString(R.string.vm_change_pin_error_invalid); + case OmtpConstants.CHANGE_PIN_MISMATCH: + return getString(R.string.vm_change_pin_error_mismatch); + case OmtpConstants.CHANGE_PIN_SYSTEM_ERROR: + return getString(R.string.vm_change_pin_error_system_error); + default: + VvmLog.wtf(TAG, "Unexpected ChangePinResult " + result); + return null; + } + } + + private void verifyOldPin() { + processPinChange(mOldPin, mOldPin); + } + + private void setNextEnabled(boolean enabled) { + mNextButton.setEnabled(enabled); + } + + + private void showError(CharSequence message) { + showError(message, null); + } + + private void showError(CharSequence message, @Nullable OnDismissListener callback) { + new AlertDialog.Builder(this) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .setOnDismissListener(callback) + .show(); + } + + /** + * Asynchronous call to change the PIN on the server. + */ + private void processPinChange(String oldPin, String newPin) { + mProgressDialog = new ProgressDialog(this); + mProgressDialog.setCancelable(false); + mProgressDialog.setMessage(getString(R.string.vm_change_pin_progress_message)); + mProgressDialog.show(); + + ChangePinNetworkRequestCallback callback = new ChangePinNetworkRequestCallback(oldPin, + newPin); + callback.requestNetwork(); + } + + private class ChangePinNetworkRequestCallback extends VvmNetworkRequestCallback { + + private final String mOldPin; + private final String mNewPin; + + public ChangePinNetworkRequestCallback(String oldPin, String newPin) { + super(mConfig, mPhoneAccountHandle, + VoicemailChangePinActivity.this.getVoicemailStatusEditor()); + mOldPin = oldPin; + mNewPin = newPin; + } + + @Override + public void onAvailable(Network network) { + super.onAvailable(network); + try (ImapHelper helper = + new ImapHelper(VoicemailChangePinActivity.this, mPhoneAccountHandle, network, + getVoicemailStatusEditor())) { + + @ChangePinResult int result = + helper.changePin(mOldPin, mNewPin); + sendResult(result); + } catch (InitializingException | MessagingException e) { + VvmLog.e(TAG, "ChangePinNetworkRequestCallback: onAvailable: ", e); + sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR); + } + } + + @Override + public void onFailed(String reason) { + super.onFailed(reason); + sendResult(OmtpConstants.CHANGE_PIN_SYSTEM_ERROR); + } + + private void sendResult(@ChangePinResult int result) { + VvmLog.i(TAG, "Change PIN result: " + result); + if (mProgressDialog.isShowing() && !VoicemailChangePinActivity.this.isDestroyed() && + !VoicemailChangePinActivity.this.isFinishing()) { + mProgressDialog.dismiss(); + } else { + VvmLog.i(TAG, "Dialog not visible, not dismissing"); + } + mHandler.obtainMessage(MESSAGE_HANDLE_RESULT, result, 0).sendToTarget(); + releaseNetwork(); + } + } + +} diff --git a/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java b/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java new file mode 100644 index 000000000..ac0df6fab --- /dev/null +++ b/java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java @@ -0,0 +1,222 @@ +/** + * Copyright (C) 2014 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.android.voicemailomtp.settings; + +import android.content.Intent; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.PreferenceScreen; +import android.preference.SwitchPreference; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import android.util.Log; +import android.view.MenuItem; + +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.R; +import com.android.voicemailomtp.SubscriptionInfoHelper; +import com.android.voicemailomtp.VisualVoicemailPreferences; + +public class VoicemailSettingsActivity extends PreferenceActivity implements + Preference.OnPreferenceChangeListener { + private static final String LOG_TAG = VoicemailSettingsActivity.class.getSimpleName(); + private static final boolean DBG = true; + + /** + * Intent action to bring up Voicemail Provider settings + * DO NOT RENAME. There are existing apps which use this intent value. + */ + public static final String ACTION_ADD_VOICEMAIL = + "com.android.voicemailomtp.CallFeaturesSetting.ADD_VOICEMAIL"; + + /** + * Intent action to bring up the {@code VoicemailSettingsActivity}. + * DO NOT RENAME. There are existing apps which use this intent value. + */ + public static final String ACTION_CONFIGURE_VOICEMAIL = + "com.android.voicemailomtp.CallFeaturesSetting.CONFIGURE_VOICEMAIL"; + + // Extra put in the return from VM provider config containing voicemail number to set + public static final String VM_NUMBER_EXTRA = "com.android.voicemailomtp.VoicemailNumber"; + // Extra put in the return from VM provider config containing call forwarding number to set + public static final String FWD_NUMBER_EXTRA = "com.android.voicemailomtp.ForwardingNumber"; + // Extra put in the return from VM provider config containing call forwarding number to set + public static final String FWD_NUMBER_TIME_EXTRA = "com.android.voicemailomtp.ForwardingNumberTime"; + // If the VM provider returns non null value in this extra we will force the user to + // choose another VM provider + public static final String SIGNOUT_EXTRA = "com.android.voicemailomtp.Signout"; + + /** + * String Extra put into ACTION_ADD_VOICEMAIL call to indicate which provider should be hidden + * in the list of providers presented to the user. This allows a provider which is being + * disabled (e.g. GV user logging out) to force the user to pick some other provider. + */ + public static final String IGNORE_PROVIDER_EXTRA = "com.android.voicemailomtp.ProviderToIgnore"; + + /** + * String Extra put into ACTION_ADD_VOICEMAIL to indicate that the voicemail setup screen should + * be opened. + */ + public static final String SETUP_VOICEMAIL_EXTRA = "com.android.voicemailomtp.SetupVoicemail"; + + /** Event for Async voicemail change call */ + private static final int EVENT_VOICEMAIL_CHANGED = 500; + private static final int EVENT_FORWARDING_CHANGED = 501; + private static final int EVENT_FORWARDING_GET_COMPLETED = 502; + + /** Handle to voicemail pref */ + private static final int VOICEMAIL_PREF_ID = 1; + private static final int VOICEMAIL_PROVIDER_CFG_ID = 2; + + /** + * Used to indicate that the voicemail preference should be shown. + */ + private boolean mShowVoicemailPreference = false; + + private int mSubId; + private PhoneAccountHandle mPhoneAccountHandle; + private SubscriptionInfoHelper mSubscriptionInfoHelper; + private OmtpVvmCarrierConfigHelper mOmtpVvmCarrierConfigHelper; + + private SwitchPreference mVoicemailVisualVoicemail; + private Preference mVoicemailChangePinPreference; + + //********************************************************************************************* + // Preference Activity Methods + //********************************************************************************************* + + @Override + protected void onCreate(Bundle icicle) { + super.onCreate(icicle); + + // Show the voicemail preference in onResume if the calling intent specifies the + // ACTION_ADD_VOICEMAIL action. + mShowVoicemailPreference = (icicle == null) && + TextUtils.equals(getIntent().getAction(), ACTION_ADD_VOICEMAIL); + + mSubscriptionInfoHelper = new SubscriptionInfoHelper(this, getIntent()); + mSubscriptionInfoHelper.setActionBarTitle( + getActionBar(), getResources(), R.string.voicemail_settings_with_label); + mSubId = mSubscriptionInfoHelper.getSubId(); + // TODO: scrap this activity. + /* + mPhoneAccountHandle = PhoneAccountHandleConverter + .fromSubId(this, mSubId); + + mOmtpVvmCarrierConfigHelper = new OmtpVvmCarrierConfigHelper( + this, mSubId); + */ + } + + @Override + protected void onResume() { + super.onResume(); + + PreferenceScreen preferenceScreen = getPreferenceScreen(); + if (preferenceScreen != null) { + preferenceScreen.removeAll(); + } + + addPreferencesFromResource(R.xml.voicemail_settings); + + PreferenceScreen prefSet = getPreferenceScreen(); + + mVoicemailVisualVoicemail = (SwitchPreference) findPreference( + getResources().getString(R.string.voicemail_visual_voicemail_key)); + + mVoicemailChangePinPreference = findPreference( + getResources().getString(R.string.voicemail_change_pin_key)); + Intent changePinIntent = new Intent(new Intent(this, VoicemailChangePinActivity.class)); + changePinIntent.putExtra(VoicemailChangePinActivity.EXTRA_PHONE_ACCOUNT_HANDLE, + mPhoneAccountHandle); + + mVoicemailChangePinPreference.setIntent(changePinIntent); + if (VoicemailChangePinActivity.isDefaultOldPinSet(this, mPhoneAccountHandle)) { + mVoicemailChangePinPreference.setTitle(R.string.voicemail_set_pin_dialog_title); + } else { + mVoicemailChangePinPreference.setTitle(R.string.voicemail_change_pin_dialog_title); + } + + if (mOmtpVvmCarrierConfigHelper.isValid()) { + mVoicemailVisualVoicemail.setOnPreferenceChangeListener(this); + mVoicemailVisualVoicemail.setChecked( + VisualVoicemailSettingsUtil.isEnabled(this, mPhoneAccountHandle)); + if (!isVisualVoicemailActivated()) { + prefSet.removePreference(mVoicemailChangePinPreference); + } + } else { + prefSet.removePreference(mVoicemailVisualVoicemail); + prefSet.removePreference(mVoicemailChangePinPreference); + } + } + + @Override + public void onPause() { + super.onPause(); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == android.R.id.home) { + onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + /** + * Implemented to support onPreferenceChangeListener to look for preference changes. + * + * @param preference is the preference to be changed + * @param objValue should be the value of the selection, NOT its localized + * display value. + */ + @Override + public boolean onPreferenceChange(Preference preference, Object objValue) { + if (DBG) log("onPreferenceChange: \"" + preference + "\" changed to \"" + objValue + "\""); + if (preference.getKey().equals(mVoicemailVisualVoicemail.getKey())) { + boolean isEnabled = (boolean) objValue; + VisualVoicemailSettingsUtil + .setEnabled(this, mPhoneAccountHandle, isEnabled); + PreferenceScreen prefSet = getPreferenceScreen(); + if (isVisualVoicemailActivated()) { + prefSet.addPreference(mVoicemailChangePinPreference); + } else { + prefSet.removePreference(mVoicemailChangePinPreference); + } + } + + // Always let the preference setting proceed. + return true; + } + + private boolean isVisualVoicemailActivated() { + if (!VisualVoicemailSettingsUtil.isEnabled(this, mPhoneAccountHandle)) { + return false; + } + VisualVoicemailPreferences preferences = new VisualVoicemailPreferences(this, + mPhoneAccountHandle); + return preferences.getString(OmtpConstants.SERVER_ADDRESS, null) != null; + + } + + private static void log(String msg) { + Log.d(LOG_TAG, msg); + } +} diff --git a/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java b/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java new file mode 100644 index 000000000..bb722bffc --- /dev/null +++ b/java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2016 Google Inc. All Rights Reserved. + * + * 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.android.voicemailomtp.sms; + +import android.content.Context; +import android.os.Bundle; +import android.telecom.PhoneAccountHandle; +import android.telephony.VisualVoicemailSms; + +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.TelephonyManagerStub; +import com.android.voicemailomtp.VvmLog; + +/** + * Class ot handle voicemail SMS under legacy mode + * + * @see OmtpVvmCarrierConfigHelper#isLegacyModeEnabled() + */ +public class LegacyModeSmsHandler { + + private static final String TAG = "LegacyModeSmsHandler"; + + public static void handle(Context context, VisualVoicemailSms sms) { + VvmLog.v(TAG, "processing VVM SMS on legacy mode"); + String eventType = sms.getPrefix(); + Bundle data = sms.getFields(); + PhoneAccountHandle handle = sms.getPhoneAccountHandle(); + + if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) { + SyncMessage message = new SyncMessage(data); + VvmLog.v(TAG, "Received SYNC sms for " + handle + + " with event " + message.getSyncTriggerEvent()); + + switch (message.getSyncTriggerEvent()) { + case OmtpConstants.NEW_MESSAGE: + case OmtpConstants.MAILBOX_UPDATE: + // The user has called into the voicemail and the new message count could + // change. + // For some carriers new message count could be set to 0 even if there are still + // unread messages, to clear the message waiting indicator. + VvmLog.v(TAG, "updating MWI"); + + // Setting voicemail message count to non-zero will show the telephony voicemail + // notification, and zero will clear it. + TelephonyManagerStub.showVoicemailNotification(message.getNewMessageCount()); + break; + default: + break; + } + } + } +} diff --git a/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java new file mode 100644 index 000000000..63af2c13d --- /dev/null +++ b/java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2015 Google Inc. All Rights Reserved. + * + * 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.android.voicemailomtp.sms; + +import android.app.PendingIntent; +import android.content.Context; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.telephony.SmsManager; +import com.android.voicemailomtp.OmtpConstants; + +/** + * An implementation of the OmtpMessageSender for T-Mobile. + */ +public class OmtpCvvmMessageSender extends OmtpMessageSender { + public OmtpCvvmMessageSender(Context context, PhoneAccountHandle phoneAccountHandle, + short applicationPort, String destinationNumber) { + super(context, phoneAccountHandle, applicationPort, destinationNumber); + } + + @Override + public void requestVvmActivation(@Nullable PendingIntent sentIntent) { + sendCvvmMessage(OmtpConstants.ACTIVATE_REQUEST, sentIntent); + } + + @Override + public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) { + sendCvvmMessage(OmtpConstants.DEACTIVATE_REQUEST, sentIntent); + } + + @Override + public void requestVvmStatus(@Nullable PendingIntent sentIntent) { + sendCvvmMessage(OmtpConstants.STATUS_REQUEST, sentIntent); + } + + private void sendCvvmMessage(String request, PendingIntent sentIntent) { + StringBuilder sb = new StringBuilder().append(request); + sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR); + appendField(sb, "dt", "15"); + sendSms(sb.toString(), sentIntent); + } +} diff --git a/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java b/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java new file mode 100644 index 000000000..c4ad2085f --- /dev/null +++ b/java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.sms; + +import android.annotation.TargetApi; +import android.content.BroadcastReceiver; +import android.content.ContentUris; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.os.UserManager; +import android.telecom.PhoneAccountHandle; +import android.telephony.VisualVoicemailSms; +import com.android.voicemailomtp.ActivationTask; +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.OmtpService; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.Voicemail; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.protocol.VisualVoicemailProtocol; +import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil; +import com.android.voicemailomtp.sync.OmtpVvmSyncService; +import com.android.voicemailomtp.sync.SyncOneTask; +import com.android.voicemailomtp.sync.SyncTask; +import com.android.voicemailomtp.sync.VoicemailsQueryHelper; +import com.android.voicemailomtp.utils.VoicemailDatabaseUtil; + +/** Receive SMS messages and send for processing by the OMTP visual voicemail source. */ +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class OmtpMessageReceiver extends BroadcastReceiver { + + private static final String TAG = "OmtpMessageReceiver"; + + private Context mContext; + + @Override + public void onReceive(Context context, Intent intent) { + mContext = context; + VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS); + PhoneAccountHandle phone = sms.getPhoneAccountHandle(); + + if (phone == null) { + // This should never happen + VvmLog.i(TAG, "Received message for null phone account"); + return; + } + + if (!context.getSystemService(UserManager.class).isUserUnlocked()) { + VvmLog.i(TAG, "Received message on locked device"); + // LegacyModeSmsHandler can handle new message notifications without storage access + LegacyModeSmsHandler.handle(context, sms); + // A full sync will happen after the device is unlocked, so nothing else need to be + // done. + return; + } + + OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(mContext, phone); + if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phone)) { + if (helper.isLegacyModeEnabled()) { + LegacyModeSmsHandler.handle(context, sms); + } else { + VvmLog.i(TAG, "Received vvm message for disabled vvm source."); + } + return; + } + + String eventType = sms.getPrefix(); + Bundle data = sms.getFields(); + + if (eventType == null || data == null) { + VvmLog.e(TAG, "Unparsable VVM SMS received, ignoring"); + return; + } + + if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) { + SyncMessage message = new SyncMessage(data); + + VvmLog.v(TAG, "Received SYNC sms for " + phone + + " with event " + message.getSyncTriggerEvent()); + processSync(phone, message); + } else if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) { + VvmLog.v(TAG, "Received Status sms for " + phone); + // If the STATUS SMS is initiated by ActivationTask the TaskSchedulerService will reject + // the follow request. Providing the data will also prevent ActivationTask from + // requesting another STATUS SMS. The following task will only run if the carrier + // spontaneous send a STATUS SMS, in that case, the VVM service should be reactivated. + ActivationTask.start(context, phone, data); + } else { + VvmLog.w(TAG, "Unknown prefix: " + eventType); + VisualVoicemailProtocol protocol = helper.getProtocol(); + if (protocol == null) { + return; + } + Bundle statusData = helper.getProtocol() + .translateStatusSmsBundle(helper, eventType, data); + if (statusData != null) { + VvmLog.i(TAG, "Protocol recognized the SMS as STATUS, activating"); + ActivationTask.start(context, phone, data); + } + } + } + + /** + * A sync message has two purposes: to signal a new voicemail message, and to indicate the + * voicemails on the server have changed remotely (usually through the TUI). Save the new + * message to the voicemail provider if it is the former case and perform a full sync in the + * latter case. + * + * @param message The sync message to extract data from. + */ + private void processSync(PhoneAccountHandle phone, SyncMessage message) { + switch (message.getSyncTriggerEvent()) { + case OmtpConstants.NEW_MESSAGE: + if (!OmtpConstants.VOICE.equals(message.getContentType())) { + VvmLog.i(TAG, "Non-voice message of type '" + message.getContentType() + + "' received, ignoring"); + return; + } + + Voicemail.Builder builder = Voicemail.createForInsertion( + message.getTimestampMillis(), message.getSender()) + .setPhoneAccount(phone) + .setSourceData(message.getId()) + .setDuration(message.getLength()) + .setSourcePackage(mContext.getPackageName()); + Voicemail voicemail = builder.build(); + + VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext); + if (queryHelper.isVoicemailUnique(voicemail)) { + Uri uri = VoicemailDatabaseUtil.insert(mContext, voicemail); + voicemail = builder.setId(ContentUris.parseId(uri)).setUri(uri).build(); + SyncOneTask.start(mContext, phone, voicemail); + } + break; + case OmtpConstants.MAILBOX_UPDATE: + SyncTask.start(mContext, phone, OmtpVvmSyncService.SYNC_DOWNLOAD_ONLY); + break; + case OmtpConstants.GREETINGS_UPDATE: + // Not implemented in V1 + break; + default: + VvmLog.e(TAG, + "Unrecognized sync trigger event: " + message.getSyncTriggerEvent()); + break; + } + } +} diff --git a/java/com/android/voicemailomtp/sms/OmtpMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpMessageSender.java new file mode 100644 index 000000000..2323e4bcf --- /dev/null +++ b/java/com/android/voicemailomtp/sms/OmtpMessageSender.java @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2015 Google Inc. All Rights Reserved. + * + * 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.android.voicemailomtp.sms; + +import android.app.PendingIntent; +import android.content.Context; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.telephony.SmsManager; +import android.telephony.VisualVoicemailService; +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.TelephonyManagerStub; +import com.android.voicemailomtp.VvmLog; +import java.io.UnsupportedEncodingException; +import java.util.Locale; + +/** + * Send client originated OMTP messages to the OMTP server. + * <p> + * Uses {@link PendingIntent} instead of a call back to notify when the message is + * sent. This is primarily to keep the implementation simple and reuse what the underlying + * {@link SmsManager} interface provides. + * <p> + * Provides simple APIs to send different types of mobile originated OMTP SMS to the VVM server. + */ +public abstract class OmtpMessageSender { + protected static final String TAG = "OmtpMessageSender"; + protected final Context mContext; + protected final PhoneAccountHandle mPhoneAccountHandle; + protected final short mApplicationPort; + protected final String mDestinationNumber; + + + public OmtpMessageSender(Context context, PhoneAccountHandle phoneAccountHandle, + short applicationPort, + String destinationNumber) { + mContext = context; + mPhoneAccountHandle = phoneAccountHandle; + mApplicationPort = applicationPort; + mDestinationNumber = destinationNumber; + } + + /** + * Sends a request to the VVM server to activate VVM for the current subscriber. + * + * @param sentIntent If not NULL this PendingIntent is broadcast when the message is + * successfully sent, or failed. + */ + public void requestVvmActivation(@Nullable PendingIntent sentIntent) {} + + /** + * Sends a request to the VVM server to deactivate VVM for the current subscriber. + * + * @param sentIntent If not NULL this PendingIntent is broadcast when the message is + * successfully sent, or failed. + */ + public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) {} + + /** + * Send a request to the VVM server to get account status of the current subscriber. + * + * @param sentIntent If not NULL this PendingIntent is broadcast when the message is + * successfully sent, or failed. + */ + public void requestVvmStatus(@Nullable PendingIntent sentIntent) {} + + protected void sendSms(String text, PendingIntent sentIntent) { + VisualVoicemailService + .sendVisualVoicemailSms(mContext, mPhoneAccountHandle, mDestinationNumber, + mApplicationPort, text, sentIntent); + } + + protected void appendField(StringBuilder sb, String field, Object value) { + sb.append(field).append(OmtpConstants.SMS_KEY_VALUE_SEPARATOR).append(value); + } +} diff --git a/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java b/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java new file mode 100644 index 000000000..aa8374781 --- /dev/null +++ b/java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2015 Google Inc. All Rights Reserved. + * + * 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.android.voicemailomtp.sms; + +import android.app.PendingIntent; +import android.content.Context; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.telephony.SmsManager; +import android.text.TextUtils; +import com.android.voicemailomtp.OmtpConstants; + +/** + * A implementation of the OmtpMessageSender using the standard OMTP sms protocol. + */ +public class OmtpStandardMessageSender extends OmtpMessageSender { + private final String mClientType; + private final String mProtocolVersion; + private final String mClientPrefix; + + /** + * Creates a new instance of OmtpStandardMessageSender. + * + * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number. + * Otherwise, a standard text SMS is sent. + * @param destinationNumber Destination number to be used. + * @param clientType The "ct" field to be set in the MO message. This is the value used by the + * VVM server to identify the client. Certain VVM servers require a specific agreed + * value for this field. + * @param protocolVersion OMTP protocol version. + * @param clientPrefix The client prefix requested to be used by the server in its MT messages. + */ + public OmtpStandardMessageSender(Context context, PhoneAccountHandle phoneAccountHandle, + short applicationPort, + String destinationNumber, String clientType, String protocolVersion, + String clientPrefix) { + super(context, phoneAccountHandle, applicationPort, destinationNumber); + mClientType = clientType; + mProtocolVersion = protocolVersion; + mClientPrefix = clientPrefix; + } + + // Activate message: + // V1.1: Activate:pv=<value>;ct=<value> + // V1.2: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix> + // V1.3: Activate:pv=<value>;ct=<value>;pt=<value>;<Clientprefix> + @Override + public void requestVvmActivation(@Nullable PendingIntent sentIntent) { + StringBuilder sb = new StringBuilder().append(OmtpConstants.ACTIVATE_REQUEST); + + appendProtocolVersionAndClientType(sb); + if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_2) || + TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) { + appendApplicationPort(sb); + appendClientPrefix(sb); + } + + sendSms(sb.toString(), sentIntent); + } + + // Deactivate message: + // V1.1: Deactivate:pv=<value>;ct=<string> + // V1.2: Deactivate:pv=<value>;ct=<string> + // V1.3: Deactivate:pv=<value>;ct=<string> + @Override + public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) { + StringBuilder sb = new StringBuilder().append(OmtpConstants.DEACTIVATE_REQUEST); + appendProtocolVersionAndClientType(sb); + + sendSms(sb.toString(), sentIntent); + } + + // Status message: + // V1.1: STATUS + // V1.2: STATUS + // V1.3: STATUS:pv=<value>;ct=<value>;pt=<value>;<Clientprefix> + @Override + public void requestVvmStatus(@Nullable PendingIntent sentIntent) { + StringBuilder sb = new StringBuilder().append(OmtpConstants.STATUS_REQUEST); + + if (TextUtils.equals(mProtocolVersion, OmtpConstants.PROTOCOL_VERSION1_3)) { + appendProtocolVersionAndClientType(sb); + appendApplicationPort(sb); + appendClientPrefix(sb); + } + + sendSms(sb.toString(), sentIntent); + } + + private void appendProtocolVersionAndClientType(StringBuilder sb) { + sb.append(OmtpConstants.SMS_PREFIX_SEPARATOR); + appendField(sb, OmtpConstants.PROTOCOL_VERSION, mProtocolVersion); + sb.append(OmtpConstants.SMS_FIELD_SEPARATOR); + appendField(sb, OmtpConstants.CLIENT_TYPE, mClientType); + } + + private void appendApplicationPort(StringBuilder sb) { + sb.append(OmtpConstants.SMS_FIELD_SEPARATOR); + appendField(sb, OmtpConstants.APPLICATION_PORT, mApplicationPort); + } + + private void appendClientPrefix(StringBuilder sb) { + sb.append(OmtpConstants.SMS_FIELD_SEPARATOR); + sb.append(mClientPrefix); + } +} diff --git a/java/com/android/voicemailomtp/sms/StatusMessage.java b/java/com/android/voicemailomtp/sms/StatusMessage.java new file mode 100644 index 000000000..3dfd4973e --- /dev/null +++ b/java/com/android/voicemailomtp/sms/StatusMessage.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.sms; + +import android.os.Bundle; +import com.android.voicemailomtp.NeededForTesting; +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.VisualVoicemailPreferences; +import com.android.voicemailomtp.VvmLog; + +/** + * Structured data representation of OMTP STATUS message. + * + * The getters will return null if the field was not set in the message body or it could not be + * parsed. + */ +public class StatusMessage { + // NOTE: Following Status SMS fields are not yet parsed, as they do not seem + // to be useful for initial omtp source implementation. + // lang, g_len, vs_len, pw_len, pm, gm, vtc, vt + + private final String mProvisioningStatus; + private final String mStatusReturnCode; + private final String mSubscriptionUrl; + private final String mServerAddress; + private final String mTuiAccessNumber; + private final String mClientSmsDestinationNumber; + private final String mImapPort; + private final String mImapUserName; + private final String mImapPassword; + private final String mSmtpPort; + private final String mSmtpUserName; + private final String mSmtpPassword; + private final String mTuiPasswordLength; + + @Override + public String toString() { + return "StatusMessage [mProvisioningStatus=" + mProvisioningStatus + + ", mStatusReturnCode=" + mStatusReturnCode + + ", mSubscriptionUrl=" + mSubscriptionUrl + + ", mServerAddress=" + mServerAddress + + ", mTuiAccessNumber=" + mTuiAccessNumber + + ", mClientSmsDestinationNumber=" + mClientSmsDestinationNumber + + ", mImapPort=" + mImapPort + + ", mImapUserName=" + mImapUserName + + ", mImapPassword=" + VvmLog.pii(mImapPassword) + + ", mSmtpPort=" + mSmtpPort + + ", mSmtpUserName=" + mSmtpUserName + + ", mSmtpPassword=" + VvmLog.pii(mSmtpPassword) + + ", mTuiPasswordLength=" + mTuiPasswordLength + "]"; + } + + public StatusMessage(Bundle wrappedData) { + mProvisioningStatus = unquote(getString(wrappedData, OmtpConstants.PROVISIONING_STATUS)); + mStatusReturnCode = getString(wrappedData, OmtpConstants.RETURN_CODE); + mSubscriptionUrl = getString(wrappedData, OmtpConstants.SUBSCRIPTION_URL); + mServerAddress = getString(wrappedData, OmtpConstants.SERVER_ADDRESS); + mTuiAccessNumber = getString(wrappedData, OmtpConstants.TUI_ACCESS_NUMBER); + mClientSmsDestinationNumber = getString(wrappedData, + OmtpConstants.CLIENT_SMS_DESTINATION_NUMBER); + mImapPort = getString(wrappedData, OmtpConstants.IMAP_PORT); + mImapUserName = getString(wrappedData, OmtpConstants.IMAP_USER_NAME); + mImapPassword = getString(wrappedData, OmtpConstants.IMAP_PASSWORD); + mSmtpPort = getString(wrappedData, OmtpConstants.SMTP_PORT); + mSmtpUserName = getString(wrappedData, OmtpConstants.SMTP_USER_NAME); + mSmtpPassword = getString(wrappedData, OmtpConstants.SMTP_PASSWORD); + mTuiPasswordLength = getString(wrappedData, OmtpConstants.TUI_PASSWORD_LENGTH); + } + + private static String unquote(String string) { + if (string.length() < 2) { + return string; + } + if (string.startsWith("\"") && string.endsWith("\"")) { + return string.substring(1, string.length() - 1); + } + return string; + } + + /** + * @return the subscriber's VVM provisioning status. + */ + public String getProvisioningStatus() { + return mProvisioningStatus; + } + + /** + * @return the return-code of the status SMS. + */ + public String getReturnCode() { + return mStatusReturnCode; + } + + /** + * @return the URL of the voicemail server. This is the URL to send the users to for subscribing + * to the visual voicemail service. + */ + @NeededForTesting + public String getSubscriptionUrl() { + return mSubscriptionUrl; + } + + /** + * @return the voicemail server address. Either server IP address or fully qualified domain + * name. + */ + public String getServerAddress() { + return mServerAddress; + } + + /** + * @return the Telephony User Interface number to call to access voicemails directly from the + * IVR. + */ + @NeededForTesting + public String getTuiAccessNumber() { + return mTuiAccessNumber; + } + + /** + * @return the number to which client originated SMSes should be sent to. + */ + @NeededForTesting + public String getClientSmsDestinationNumber() { + return mClientSmsDestinationNumber; + } + + /** + * @return the IMAP server port to talk to. + */ + public String getImapPort() { + return mImapPort; + } + + /** + * @return the IMAP user name to be used for authentication. + */ + public String getImapUserName() { + return mImapUserName; + } + + /** + * @return the IMAP password to be used for authentication. + */ + public String getImapPassword() { + return mImapPassword; + } + + /** + * @return the SMTP server port to talk to. + */ + @NeededForTesting + public String getSmtpPort() { + return mSmtpPort; + } + + /** + * @return the SMTP user name to be used for SMTP authentication. + */ + @NeededForTesting + public String getSmtpUserName() { + return mSmtpUserName; + } + + /** + * @return the SMTP password to be used for SMTP authentication. + */ + @NeededForTesting + public String getSmtpPassword() { + return mSmtpPassword; + } + + public String getTuiPasswordLength() { + return mTuiPasswordLength; + } + + private static String getString(Bundle bundle, String key) { + String value = bundle.getString(key); + if (value == null) { + return ""; + } + return value; + } + + /** + * Saves a StatusMessage to the {@link VisualVoicemailPreferences}. Not all fields are saved. + */ + public VisualVoicemailPreferences.Editor putStatus(VisualVoicemailPreferences.Editor editor) { + return editor + .putString(OmtpConstants.IMAP_PORT, getImapPort()) + .putString(OmtpConstants.SERVER_ADDRESS, getServerAddress()) + .putString(OmtpConstants.IMAP_USER_NAME, getImapUserName()) + .putString(OmtpConstants.IMAP_PASSWORD, getImapPassword()) + .putString(OmtpConstants.TUI_PASSWORD_LENGTH, getTuiPasswordLength()); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java b/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java new file mode 100644 index 000000000..4e10c0e43 --- /dev/null +++ b/java/com/android/voicemailomtp/sms/StatusSmsFetcher.java @@ -0,0 +1,162 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.sms; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Build.VERSION_CODES; +import android.os.Bundle; +import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; +import android.telecom.PhoneAccountHandle; +import android.telephony.SmsManager; +import android.telephony.VisualVoicemailSms; +import com.android.voicemailomtp.Assert; +import com.android.voicemailomtp.OmtpConstants; +import com.android.voicemailomtp.OmtpService; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.protocol.VisualVoicemailProtocol; +import java.io.Closeable; +import java.io.IOException; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** Intercepts a incoming STATUS SMS with a blocking call. */ +@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/ +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class StatusSmsFetcher extends BroadcastReceiver implements Closeable { + + private static final String TAG = "VvmStatusSmsFetcher"; + + private static final long STATUS_SMS_TIMEOUT_MILLIS = 60_000; + + private static final String ACTION_REQUEST_SENT_INTENT + = "com.android.voicemailomtp.sms.REQUEST_SENT"; + private static final int ACTION_REQUEST_SENT_REQUEST_CODE = 0; + + private CompletableFuture<Bundle> mFuture = new CompletableFuture<>(); + + private final Context mContext; + private final PhoneAccountHandle mPhoneAccountHandle; + + public StatusSmsFetcher(Context context, PhoneAccountHandle phoneAccountHandle) { + mContext = context; + mPhoneAccountHandle = phoneAccountHandle; + IntentFilter filter = new IntentFilter(ACTION_REQUEST_SENT_INTENT); + filter.addAction(OmtpService.ACTION_SMS_RECEIVED); + context.registerReceiver(this, filter); + } + + @Override + public void close() throws IOException { + mContext.unregisterReceiver(this); + } + + @WorkerThread + @Nullable + public Bundle get() throws InterruptedException, ExecutionException, TimeoutException, + CancellationException { + Assert.isNotMainThread(); + return mFuture.get(STATUS_SMS_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } + + public PendingIntent getSentIntent() { + Intent intent = new Intent(ACTION_REQUEST_SENT_INTENT); + intent.setPackage(mContext.getPackageName()); + // Because the receiver is registered dynamically, implicit intent must be used. + // There should only be a single status SMS request at a time. + return PendingIntent.getBroadcast(mContext, ACTION_REQUEST_SENT_REQUEST_CODE, intent, + PendingIntent.FLAG_CANCEL_CURRENT); + } + + @Override + @MainThread + public void onReceive(Context context, Intent intent) { + Assert.isMainThread(); + if (ACTION_REQUEST_SENT_INTENT.equals(intent.getAction())) { + int resultCode = getResultCode(); + + if (resultCode == Activity.RESULT_OK) { + VvmLog.d(TAG, "Request SMS successfully sent"); + return; + } + + VvmLog.e(TAG, "Request SMS send failed: " + sentSmsResultToString(resultCode)); + mFuture.cancel(true); + return; + } + + VisualVoicemailSms sms = intent.getExtras().getParcelable(OmtpService.EXTRA_VOICEMAIL_SMS); + + if (!mPhoneAccountHandle.equals(sms.getPhoneAccountHandle())) { + return; + } + String eventType = sms.getPrefix(); + + if (eventType.equals(OmtpConstants.STATUS_SMS_PREFIX)) { + mFuture.complete(sms.getFields()); + return; + } + + if (eventType.equals(OmtpConstants.SYNC_SMS_PREFIX)) { + return; + } + + VvmLog.i(TAG, "VVM SMS with event " + eventType + + " received, attempting to translate to STATUS SMS"); + OmtpVvmCarrierConfigHelper helper = new OmtpVvmCarrierConfigHelper(context, + mPhoneAccountHandle); + VisualVoicemailProtocol protocol = helper.getProtocol(); + if (protocol == null) { + return; + } + Bundle translatedBundle = protocol.translateStatusSmsBundle(helper, eventType, + sms.getFields()); + + if (translatedBundle != null) { + VvmLog.i(TAG, "Translated to STATUS SMS"); + mFuture.complete(translatedBundle); + } + } + + private static String sentSmsResultToString(int resultCode) { + switch (resultCode) { + case Activity.RESULT_OK: + return "OK"; + case SmsManager.RESULT_ERROR_GENERIC_FAILURE: + return "RESULT_ERROR_GENERIC_FAILURE"; + case SmsManager.RESULT_ERROR_NO_SERVICE: + return "RESULT_ERROR_GENERIC_FAILURE"; + case SmsManager.RESULT_ERROR_NULL_PDU: + return "RESULT_ERROR_GENERIC_FAILURE"; + case SmsManager.RESULT_ERROR_RADIO_OFF: + return "RESULT_ERROR_GENERIC_FAILURE"; + default: + return "UNKNOWN CODE: " + resultCode; + } + } +} diff --git a/java/com/android/voicemailomtp/sms/SyncMessage.java b/java/com/android/voicemailomtp/sms/SyncMessage.java new file mode 100644 index 000000000..89cfc0f19 --- /dev/null +++ b/java/com/android/voicemailomtp/sms/SyncMessage.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.sms; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import com.android.voicemailomtp.NeededForTesting; +import com.android.voicemailomtp.OmtpConstants; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Locale; + +/** + * Structured data representation of an OMTP SYNC message. + * + * Getters will return null if the field was not set in the message body or it could not be parsed. + */ +public class SyncMessage { + // Sync event that triggered this message. + private final String mSyncTriggerEvent; + // Total number of new messages on the server. + private final int mNewMessageCount; + // UID of the new message. + private final String mMessageId; + // Length of the message. + private final int mMessageLength; + // Content type (voice, video, fax...) of the new message. + private final String mContentType; + // Sender of the new message. + private final String mSender; + // Timestamp (in millis) of the new message. + private final long mMsgTimeMillis; + + @Override + public String toString() { + return "SyncMessage [mSyncTriggerEvent=" + mSyncTriggerEvent + + ", mNewMessageCount=" + mNewMessageCount + + ", mMessageId=" + mMessageId + + ", mMessageLength=" + mMessageLength + + ", mContentType=" + mContentType + + ", mSender=" + mSender + + ", mMsgTimeMillis=" + mMsgTimeMillis + "]"; + } + + public SyncMessage(Bundle wrappedData) { + mSyncTriggerEvent = getString(wrappedData, OmtpConstants.SYNC_TRIGGER_EVENT); + mMessageId = getString(wrappedData, OmtpConstants.MESSAGE_UID); + mMessageLength = getInt(wrappedData, OmtpConstants.MESSAGE_LENGTH); + mContentType = getString(wrappedData, OmtpConstants.CONTENT_TYPE); + mSender = getString(wrappedData, OmtpConstants.SENDER); + mNewMessageCount = getInt(wrappedData, OmtpConstants.NUM_MESSAGE_COUNT); + mMsgTimeMillis = parseTime(wrappedData.getString(OmtpConstants.TIME)); + } + + private static long parseTime(@Nullable String value) { + if (value == null) { + return 0L; + } + try { + return new SimpleDateFormat( + OmtpConstants.DATE_TIME_FORMAT, Locale.US) + .parse(value).getTime(); + } catch (ParseException e) { + return 0L; + } + } + /** + * @return the event that triggered the sync message. This is a mandatory field and must always + * be set. + */ + public String getSyncTriggerEvent() { + return mSyncTriggerEvent; + } + + /** + * @return the number of new messages stored on the voicemail server. + */ + @NeededForTesting + public int getNewMessageCount() { + return mNewMessageCount; + } + + /** + * @return the message ID of the new message. + * <p> + * Expected to be set only for + * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE} + */ + public String getId() { + return mMessageId; + } + + /** + * @return the content type of the new message. + * <p> + * Expected to be set only for + * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE} + */ + @NeededForTesting + public String getContentType() { + return mContentType; + } + + /** + * @return the message length of the new message. + * <p> + * Expected to be set only for + * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE} + */ + public int getLength() { + return mMessageLength; + } + + /** + * @return the sender's phone number of the new message specified as MSISDN. + * <p> + * Expected to be set only for + * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE} + */ + public String getSender() { + return mSender; + } + + /** + * @return the timestamp as milliseconds for the new message. + * <p> + * Expected to be set only for + * {@link com.android.voicemailomtp.OmtpConstants#NEW_MESSAGE} + */ + public long getTimestampMillis() { + return mMsgTimeMillis; + } + + private static int getInt(Bundle wrappedData, String key) { + String value = wrappedData.getString(key); + if (value == null) { + return 0; + } + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return 0; + } + } + + private static String getString(Bundle wrappedData, String key) { + String value = wrappedData.getString(key); + if (value == null) { + return ""; + } + return value; + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/sms/Vvm3MessageSender.java b/java/com/android/voicemailomtp/sms/Vvm3MessageSender.java new file mode 100644 index 000000000..02e465967 --- /dev/null +++ b/java/com/android/voicemailomtp/sms/Vvm3MessageSender.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.sms; + +import android.app.PendingIntent; +import android.content.Context; +import android.support.annotation.Nullable; +import android.telecom.PhoneAccountHandle; +import android.telephony.SmsManager; + +public class Vvm3MessageSender extends OmtpMessageSender { + + /** + * Creates a new instance of Vvm3MessageSender. + * + * @param applicationPort If set to a value > 0 then a binary sms is sent to this port number. + * Otherwise, a standard text SMS is sent. + */ + public Vvm3MessageSender(Context context, PhoneAccountHandle phoneAccountHandle, + short applicationPort, String destinationNumber) { + super(context, phoneAccountHandle, applicationPort, destinationNumber); + } + + @Override + public void requestVvmActivation(@Nullable PendingIntent sentIntent) { + // Activation not supported for VVM3, send a status request instead. + requestVvmStatus(sentIntent); + } + + @Override + public void requestVvmDeactivation(@Nullable PendingIntent sentIntent) { + // Deactivation not supported for VVM3, do nothing + } + + + @Override + public void requestVvmStatus(@Nullable PendingIntent sentIntent) { + // Status message: + // STATUS + StringBuilder sb = new StringBuilder().append("STATUS"); + sendSms(sb.toString(), sentIntent); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java b/java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java new file mode 100644 index 000000000..b41450790 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java @@ -0,0 +1,1202 @@ +/* + * 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 org.apache.commons.io; + +import java.io.BufferedInputStream; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.CharArrayWriter; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.Reader; +import java.io.StringWriter; +import java.io.Writer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; + +/** + * General IO stream manipulation utilities. + * <p> + * This class provides static utility methods for input/output operations. + * <ul> + * <li>closeQuietly - these methods close a stream ignoring nulls and exceptions + * <li>toXxx/read - these methods read data from a stream + * <li>write - these methods write data to a stream + * <li>copy - these methods copy all the data from one stream to another + * <li>contentEquals - these methods compare the content of two streams + * </ul> + * <p> + * The byte-to-char methods and char-to-byte methods involve a conversion step. + * Two methods are provided in each case, one that uses the platform default + * encoding and the other which allows you to specify an encoding. You are + * encouraged to always specify an encoding because relying on the platform + * default can lead to unexpected results, for example when moving from + * development to production. + * <p> + * All the methods in this class that read a stream are buffered internally. + * This means that there is no cause to use a <code>BufferedInputStream</code> + * or <code>BufferedReader</code>. The default buffer size of 4K has been shown + * to be efficient in tests. + * <p> + * Wherever possible, the methods in this class do <em>not</em> flush or close + * the stream. This is to avoid making non-portable assumptions about the + * streams' origin and further use. Thus the caller is still responsible for + * closing streams after use. + * <p> + * Origin of code: Excalibur. + * + * @author Peter Donald + * @author Jeff Turner + * @author Matthew Hawthorne + * @author Stephen Colebourne + * @author Gareth Davis + * @author Ian Springer + * @author Niall Pemberton + * @author Sandy McArthur + * @version $Id: IOUtils.java 481854 2006-12-03 18:30:07Z scolebourne $ + */ +public class IOUtils { + // NOTE: This class is focussed on InputStream, OutputStream, Reader and + // Writer. Each method should take at least one of these as a parameter, + // or return one of them. + + /** + * The Unix directory separator character. + */ + public static final char DIR_SEPARATOR_UNIX = '/'; + /** + * The Windows directory separator character. + */ + public static final char DIR_SEPARATOR_WINDOWS = '\\'; + /** + * The system directory separator character. + */ + public static final char DIR_SEPARATOR = File.separatorChar; + /** + * The Unix line separator string. + */ + public static final String LINE_SEPARATOR_UNIX = "\n"; + /** + * The Windows line separator string. + */ + public static final String LINE_SEPARATOR_WINDOWS = "\r\n"; + /** + * The system line separator string. + */ + public static final String LINE_SEPARATOR; + static { + // avoid security issues + StringWriter buf = new StringWriter(4); + PrintWriter out = new PrintWriter(buf); + out.println(); + LINE_SEPARATOR = buf.toString(); + } + + /** + * The default buffer size to use. + */ + private static final int DEFAULT_BUFFER_SIZE = 1024 * 4; + + /** + * Instances should NOT be constructed in standard programming. + */ + public IOUtils() { + super(); + } + + //----------------------------------------------------------------------- + /** + * Unconditionally close an <code>Reader</code>. + * <p> + * Equivalent to {@link Reader#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + * + * @param input the Reader to close, may be null or already closed + */ + public static void closeQuietly(Reader input) { + try { + if (input != null) { + input.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + /** + * Unconditionally close a <code>Writer</code>. + * <p> + * Equivalent to {@link Writer#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + * + * @param output the Writer to close, may be null or already closed + */ + public static void closeQuietly(Writer output) { + try { + if (output != null) { + output.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + /** + * Unconditionally close an <code>InputStream</code>. + * <p> + * Equivalent to {@link InputStream#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + * + * @param input the InputStream to close, may be null or already closed + */ + public static void closeQuietly(InputStream input) { + try { + if (input != null) { + input.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + /** + * Unconditionally close an <code>OutputStream</code>. + * <p> + * Equivalent to {@link OutputStream#close()}, except any exceptions will be ignored. + * This is typically used in finally blocks. + * + * @param output the OutputStream to close, may be null or already closed + */ + public static void closeQuietly(OutputStream output) { + try { + if (output != null) { + output.close(); + } + } catch (IOException ioe) { + // ignore + } + } + + // read toByteArray + //----------------------------------------------------------------------- + /** + * Get the contents of an <code>InputStream</code> as a <code>byte[]</code>. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * + * @param input the <code>InputStream</code> to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(InputStream input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + /** + * Get the contents of a <code>Reader</code> as a <code>byte[]</code> + * using the default character encoding of the platform. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedReader</code>. + * + * @param input the <code>Reader</code> to read from + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static byte[] toByteArray(Reader input) throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output); + return output.toByteArray(); + } + + /** + * Get the contents of a <code>Reader</code> as a <code>byte[]</code> + * using the specified character encoding. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedReader</code>. + * + * @param input the <code>Reader</code> to read from + * @param encoding the encoding to use, null means platform default + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static byte[] toByteArray(Reader input, String encoding) + throws IOException { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + copy(input, output, encoding); + return output.toByteArray(); + } + + /** + * Get the contents of a <code>String</code> as a <code>byte[]</code> + * using the default character encoding of the platform. + * <p> + * This is the same as {@link String#getBytes()}. + * + * @param input the <code>String</code> to convert + * @return the requested byte array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs (never occurs) + * @deprecated Use {@link String#getBytes()} + */ + @Deprecated + public static byte[] toByteArray(String input) throws IOException { + return input.getBytes(); + } + + // read char[] + //----------------------------------------------------------------------- + /** + * Get the contents of an <code>InputStream</code> as a character array + * using the default character encoding of the platform. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * + * @param is the <code>InputStream</code> to read from + * @return the requested character array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static char[] toCharArray(InputStream is) throws IOException { + CharArrayWriter output = new CharArrayWriter(); + copy(is, output); + return output.toCharArray(); + } + + /** + * Get the contents of an <code>InputStream</code> as a character array + * using the specified character encoding. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * + * @param is the <code>InputStream</code> to read from + * @param encoding the encoding to use, null means platform default + * @return the requested character array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static char[] toCharArray(InputStream is, String encoding) + throws IOException { + CharArrayWriter output = new CharArrayWriter(); + copy(is, output, encoding); + return output.toCharArray(); + } + + /** + * Get the contents of a <code>Reader</code> as a character array. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedReader</code>. + * + * @param input the <code>Reader</code> to read from + * @return the requested character array + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static char[] toCharArray(Reader input) throws IOException { + CharArrayWriter sw = new CharArrayWriter(); + copy(input, sw); + return sw.toCharArray(); + } + + // read toString + //----------------------------------------------------------------------- + /** + * Get the contents of an <code>InputStream</code> as a String + * using the default character encoding of the platform. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * + * @param input the <code>InputStream</code> to read from + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static String toString(InputStream input) throws IOException { + StringWriter sw = new StringWriter(); + copy(input, sw); + return sw.toString(); + } + + /** + * Get the contents of an <code>InputStream</code> as a String + * using the specified character encoding. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * + * @param input the <code>InputStream</code> to read from + * @param encoding the encoding to use, null means platform default + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static String toString(InputStream input, String encoding) + throws IOException { + StringWriter sw = new StringWriter(); + copy(input, sw, encoding); + return sw.toString(); + } + + /** + * Get the contents of a <code>Reader</code> as a String. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedReader</code>. + * + * @param input the <code>Reader</code> to read from + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + */ + public static String toString(Reader input) throws IOException { + StringWriter sw = new StringWriter(); + copy(input, sw); + return sw.toString(); + } + + /** + * Get the contents of a <code>byte[]</code> as a String + * using the default character encoding of the platform. + * + * @param input the byte array to read from + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs (never occurs) + * @deprecated Use {@link String#String(byte[])} + */ + @Deprecated + public static String toString(byte[] input) throws IOException { + return new String(input); + } + + /** + * Get the contents of a <code>byte[]</code> as a String + * using the specified character encoding. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * + * @param input the byte array to read from + * @param encoding the encoding to use, null means platform default + * @return the requested String + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs (never occurs) + * @deprecated Use {@link String#String(byte[],String)} + */ + @Deprecated + public static String toString(byte[] input, String encoding) + throws IOException { + if (encoding == null) { + return new String(input); + } else { + return new String(input, encoding); + } + } + + // readLines + //----------------------------------------------------------------------- + /** + * Get the contents of an <code>InputStream</code> as a list of Strings, + * one entry per line, using the default character encoding of the platform. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * + * @param input the <code>InputStream</code> to read from, not null + * @return the list of Strings, never null + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static List<String> readLines(InputStream input) throws IOException { + InputStreamReader reader = new InputStreamReader(input); + return readLines(reader); + } + + /** + * Get the contents of an <code>InputStream</code> as a list of Strings, + * one entry per line, using the specified character encoding. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * + * @param input the <code>InputStream</code> to read from, not null + * @param encoding the encoding to use, null means platform default + * @return the list of Strings, never null + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static List<String> readLines(InputStream input, String encoding) throws IOException { + if (encoding == null) { + return readLines(input); + } else { + InputStreamReader reader = new InputStreamReader(input, encoding); + return readLines(reader); + } + } + + /** + * Get the contents of a <code>Reader</code> as a list of Strings, + * one entry per line. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedReader</code>. + * + * @param input the <code>Reader</code> to read from, not null + * @return the list of Strings, never null + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static List<String> readLines(Reader input) throws IOException { + BufferedReader reader = new BufferedReader(input); + List<String> list = new ArrayList<String>(); + String line = reader.readLine(); + while (line != null) { + list.add(line); + line = reader.readLine(); + } + return list; + } + + //----------------------------------------------------------------------- + /** + * Convert the specified string to an input stream, encoded as bytes + * using the default character encoding of the platform. + * + * @param input the string to convert + * @return an input stream + * @since Commons IO 1.1 + */ + public static InputStream toInputStream(String input) { + byte[] bytes = input.getBytes(); + return new ByteArrayInputStream(bytes); + } + + /** + * Convert the specified string to an input stream, encoded as bytes + * using the specified character encoding. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * + * @param input the string to convert + * @param encoding the encoding to use, null means platform default + * @throws IOException if the encoding is invalid + * @return an input stream + * @since Commons IO 1.1 + */ + public static InputStream toInputStream(String input, String encoding) throws IOException { + byte[] bytes = encoding != null ? input.getBytes(encoding) : input.getBytes(); + return new ByteArrayInputStream(bytes); + } + + // write byte[] + //----------------------------------------------------------------------- + /** + * Writes bytes from a <code>byte[]</code> to an <code>OutputStream</code>. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the <code>OutputStream</code> to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(byte[] data, OutputStream output) + throws IOException { + if (data != null) { + output.write(data); + } + } + + /** + * Writes bytes from a <code>byte[]</code> to chars on a <code>Writer</code> + * using the default character encoding of the platform. + * <p> + * This method uses {@link String#String(byte[])}. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the <code>Writer</code> to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(byte[] data, Writer output) throws IOException { + if (data != null) { + output.write(new String(data)); + } + } + + /** + * Writes bytes from a <code>byte[]</code> to chars on a <code>Writer</code> + * using the specified character encoding. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * <p> + * This method uses {@link String#String(byte[], String)}. + * + * @param data the byte array to write, do not modify during output, + * null ignored + * @param output the <code>Writer</code> to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(byte[] data, Writer output, String encoding) + throws IOException { + if (data != null) { + if (encoding == null) { + write(data, output); + } else { + output.write(new String(data, encoding)); + } + } + } + + // write char[] + //----------------------------------------------------------------------- + /** + * Writes chars from a <code>char[]</code> to a <code>Writer</code> + * using the default character encoding of the platform. + * + * @param data the char array to write, do not modify during output, + * null ignored + * @param output the <code>Writer</code> to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(char[] data, Writer output) throws IOException { + if (data != null) { + output.write(data); + } + } + + /** + * Writes chars from a <code>char[]</code> to bytes on an + * <code>OutputStream</code>. + * <p> + * This method uses {@link String#String(char[])} and + * {@link String#getBytes()}. + * + * @param data the char array to write, do not modify during output, + * null ignored + * @param output the <code>OutputStream</code> to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(char[] data, OutputStream output) + throws IOException { + if (data != null) { + output.write(new String(data).getBytes()); + } + } + + /** + * Writes chars from a <code>char[]</code> to bytes on an + * <code>OutputStream</code> using the specified character encoding. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * <p> + * This method uses {@link String#String(char[])} and + * {@link String#getBytes(String)}. + * + * @param data the char array to write, do not modify during output, + * null ignored + * @param output the <code>OutputStream</code> to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(char[] data, OutputStream output, String encoding) + throws IOException { + if (data != null) { + if (encoding == null) { + write(data, output); + } else { + output.write(new String(data).getBytes(encoding)); + } + } + } + + // write String + //----------------------------------------------------------------------- + /** + * Writes chars from a <code>String</code> to a <code>Writer</code>. + * + * @param data the <code>String</code> to write, null ignored + * @param output the <code>Writer</code> to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(String data, Writer output) throws IOException { + if (data != null) { + output.write(data); + } + } + + /** + * Writes chars from a <code>String</code> to bytes on an + * <code>OutputStream</code> using the default character encoding of the + * platform. + * <p> + * This method uses {@link String#getBytes()}. + * + * @param data the <code>String</code> to write, null ignored + * @param output the <code>OutputStream</code> to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(String data, OutputStream output) + throws IOException { + if (data != null) { + output.write(data.getBytes()); + } + } + + /** + * Writes chars from a <code>String</code> to bytes on an + * <code>OutputStream</code> using the specified character encoding. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * <p> + * This method uses {@link String#getBytes(String)}. + * + * @param data the <code>String</code> to write, null ignored + * @param output the <code>OutputStream</code> to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(String data, OutputStream output, String encoding) + throws IOException { + if (data != null) { + if (encoding == null) { + write(data, output); + } else { + output.write(data.getBytes(encoding)); + } + } + } + + // write StringBuffer + //----------------------------------------------------------------------- + /** + * Writes chars from a <code>StringBuffer</code> to a <code>Writer</code>. + * + * @param data the <code>StringBuffer</code> to write, null ignored + * @param output the <code>Writer</code> to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(StringBuffer data, Writer output) + throws IOException { + if (data != null) { + output.write(data.toString()); + } + } + + /** + * Writes chars from a <code>StringBuffer</code> to bytes on an + * <code>OutputStream</code> using the default character encoding of the + * platform. + * <p> + * This method uses {@link String#getBytes()}. + * + * @param data the <code>StringBuffer</code> to write, null ignored + * @param output the <code>OutputStream</code> to write to + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(StringBuffer data, OutputStream output) + throws IOException { + if (data != null) { + output.write(data.toString().getBytes()); + } + } + + /** + * Writes chars from a <code>StringBuffer</code> to bytes on an + * <code>OutputStream</code> using the specified character encoding. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * <p> + * This method uses {@link String#getBytes(String)}. + * + * @param data the <code>StringBuffer</code> to write, null ignored + * @param output the <code>OutputStream</code> to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void write(StringBuffer data, OutputStream output, + String encoding) throws IOException { + if (data != null) { + if (encoding == null) { + write(data, output); + } else { + output.write(data.toString().getBytes(encoding)); + } + } + } + + // writeLines + //----------------------------------------------------------------------- + /** + * Writes the <code>toString()</code> value of each item in a collection to + * an <code>OutputStream</code> line by line, using the default character + * encoding of the platform and the specified line ending. + * + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @param output the <code>OutputStream</code> to write to, not null, not closed + * @throws NullPointerException if the output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void writeLines(Collection<Object> lines, String lineEnding, + OutputStream output) throws IOException { + if (lines == null) { + return; + } + if (lineEnding == null) { + lineEnding = LINE_SEPARATOR; + } + for (Iterator<Object> it = lines.iterator(); it.hasNext(); ) { + Object line = it.next(); + if (line != null) { + output.write(line.toString().getBytes()); + } + output.write(lineEnding.getBytes()); + } + } + + /** + * Writes the <code>toString()</code> value of each item in a collection to + * an <code>OutputStream</code> line by line, using the specified character + * encoding and the specified line ending. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @param output the <code>OutputStream</code> to write to, not null, not closed + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void writeLines(Collection<Object> lines, String lineEnding, + OutputStream output, String encoding) throws IOException { + if (encoding == null) { + writeLines(lines, lineEnding, output); + } else { + if (lines == null) { + return; + } + if (lineEnding == null) { + lineEnding = LINE_SEPARATOR; + } + for (Iterator<Object> it = lines.iterator(); it.hasNext(); ) { + Object line = it.next(); + if (line != null) { + output.write(line.toString().getBytes(encoding)); + } + output.write(lineEnding.getBytes(encoding)); + } + } + } + + /** + * Writes the <code>toString()</code> value of each item in a collection to + * a <code>Writer</code> line by line, using the specified line ending. + * + * @param lines the lines to write, null entries produce blank lines + * @param lineEnding the line separator to use, null is system default + * @param writer the <code>Writer</code> to write to, not null, not closed + * @throws NullPointerException if the input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void writeLines(Collection<Object> lines, String lineEnding, + Writer writer) throws IOException { + if (lines == null) { + return; + } + if (lineEnding == null) { + lineEnding = LINE_SEPARATOR; + } + for (Iterator<Object> it = lines.iterator(); it.hasNext(); ) { + Object line = it.next(); + if (line != null) { + writer.write(line.toString()); + } + writer.write(lineEnding); + } + } + + // copy from InputStream + //----------------------------------------------------------------------- + /** + * Copy bytes from an <code>InputStream</code> to an + * <code>OutputStream</code>. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * <p> + * Large streams (over 2GB) will return a bytes copied value of + * <code>-1</code> after the copy has completed since the correct + * number of bytes cannot be returned as an int. For large streams + * use the <code>copyLarge(InputStream, OutputStream)</code> method. + * + * @param input the <code>InputStream</code> to read from + * @param output the <code>OutputStream</code> to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws ArithmeticException if the byte count is too large + * @since Commons IO 1.1 + */ + public static int copy(InputStream input, OutputStream output) throws IOException { + long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copy bytes from a large (over 2GB) <code>InputStream</code> to an + * <code>OutputStream</code>. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * + * @param input the <code>InputStream</code> to read from + * @param output the <code>OutputStream</code> to write to + * @return the number of bytes copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.3 + */ + public static long copyLarge(InputStream input, OutputStream output) + throws IOException { + byte[] buffer = new byte[DEFAULT_BUFFER_SIZE]; + long count = 0; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Copy bytes from an <code>InputStream</code> to chars on a + * <code>Writer</code> using the default character encoding of the platform. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * <p> + * This method uses {@link InputStreamReader}. + * + * @param input the <code>InputStream</code> to read from + * @param output the <code>Writer</code> to write to + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void copy(InputStream input, Writer output) + throws IOException { + InputStreamReader in = new InputStreamReader(input); + copy(in, output); + } + + /** + * Copy bytes from an <code>InputStream</code> to chars on a + * <code>Writer</code> using the specified character encoding. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedInputStream</code>. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * <p> + * This method uses {@link InputStreamReader}. + * + * @param input the <code>InputStream</code> to read from + * @param output the <code>Writer</code> to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void copy(InputStream input, Writer output, String encoding) + throws IOException { + if (encoding == null) { + copy(input, output); + } else { + InputStreamReader in = new InputStreamReader(input, encoding); + copy(in, output); + } + } + + // copy from Reader + //----------------------------------------------------------------------- + /** + * Copy chars from a <code>Reader</code> to a <code>Writer</code>. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedReader</code>. + * <p> + * Large streams (over 2GB) will return a chars copied value of + * <code>-1</code> after the copy has completed since the correct + * number of chars cannot be returned as an int. For large streams + * use the <code>copyLarge(Reader, Writer)</code> method. + * + * @param input the <code>Reader</code> to read from + * @param output the <code>Writer</code> to write to + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @throws ArithmeticException if the character count is too large + * @since Commons IO 1.1 + */ + public static int copy(Reader input, Writer output) throws IOException { + long count = copyLarge(input, output); + if (count > Integer.MAX_VALUE) { + return -1; + } + return (int) count; + } + + /** + * Copy chars from a large (over 2GB) <code>Reader</code> to a <code>Writer</code>. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedReader</code>. + * + * @param input the <code>Reader</code> to read from + * @param output the <code>Writer</code> to write to + * @return the number of characters copied + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.3 + */ + public static long copyLarge(Reader input, Writer output) throws IOException { + char[] buffer = new char[DEFAULT_BUFFER_SIZE]; + long count = 0; + int n = 0; + while (-1 != (n = input.read(buffer))) { + output.write(buffer, 0, n); + count += n; + } + return count; + } + + /** + * Copy chars from a <code>Reader</code> to bytes on an + * <code>OutputStream</code> using the default character encoding of the + * platform, and calling flush. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedReader</code>. + * <p> + * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + * <p> + * This method uses {@link OutputStreamWriter}. + * + * @param input the <code>Reader</code> to read from + * @param output the <code>OutputStream</code> to write to + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void copy(Reader input, OutputStream output) + throws IOException { + OutputStreamWriter out = new OutputStreamWriter(output); + copy(input, out); + // XXX Unless anyone is planning on rewriting OutputStreamWriter, we + // have to flush here. + out.flush(); + } + + /** + * Copy chars from a <code>Reader</code> to bytes on an + * <code>OutputStream</code> using the specified character encoding, and + * calling flush. + * <p> + * This method buffers the input internally, so there is no need to use a + * <code>BufferedReader</code>. + * <p> + * Character encoding names can be found at + * <a href="http://www.iana.org/assignments/character-sets">IANA</a>. + * <p> + * Due to the implementation of OutputStreamWriter, this method performs a + * flush. + * <p> + * This method uses {@link OutputStreamWriter}. + * + * @param input the <code>Reader</code> to read from + * @param output the <code>OutputStream</code> to write to + * @param encoding the encoding to use, null means platform default + * @throws NullPointerException if the input or output is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static void copy(Reader input, OutputStream output, String encoding) + throws IOException { + if (encoding == null) { + copy(input, output); + } else { + OutputStreamWriter out = new OutputStreamWriter(output, encoding); + copy(input, out); + // XXX Unless anyone is planning on rewriting OutputStreamWriter, + // we have to flush here. + out.flush(); + } + } + + // content equals + //----------------------------------------------------------------------- + /** + * Compare the contents of two Streams to determine if they are equal or + * not. + * <p> + * This method buffers the input internally using + * <code>BufferedInputStream</code> if they are not already buffered. + * + * @param input1 the first stream + * @param input2 the second stream + * @return true if the content of the streams are equal or they both don't + * exist, false otherwise + * @throws NullPointerException if either input is null + * @throws IOException if an I/O error occurs + */ + public static boolean contentEquals(InputStream input1, InputStream input2) + throws IOException { + if (!(input1 instanceof BufferedInputStream)) { + input1 = new BufferedInputStream(input1); + } + if (!(input2 instanceof BufferedInputStream)) { + input2 = new BufferedInputStream(input2); + } + + int ch = input1.read(); + while (-1 != ch) { + int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + int ch2 = input2.read(); + return (ch2 == -1); + } + + /** + * Compare the contents of two Readers to determine if they are equal or + * not. + * <p> + * This method buffers the input internally using + * <code>BufferedReader</code> if they are not already buffered. + * + * @param input1 the first reader + * @param input2 the second reader + * @return true if the content of the readers are equal or they both don't + * exist, false otherwise + * @throws NullPointerException if either input is null + * @throws IOException if an I/O error occurs + * @since Commons IO 1.1 + */ + public static boolean contentEquals(Reader input1, Reader input2) + throws IOException { + if (!(input1 instanceof BufferedReader)) { + input1 = new BufferedReader(input1); + } + if (!(input2 instanceof BufferedReader)) { + input2 = new BufferedReader(input2); + } + + int ch = input1.read(); + while (-1 != ch) { + int ch2 = input2.read(); + if (ch != ch2) { + return false; + } + ch = input1.read(); + } + + int ch2 = input2.read(); + return (ch2 == -1); + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java new file mode 100644 index 000000000..867c43d86 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java @@ -0,0 +1,392 @@ +/**************************************************************** + * 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 org.apache.james.mime4j; + +import java.util.HashMap; +import java.util.Map; + +/** + * Encapsulates the values of the MIME-specific header fields + * (which starts with <code>Content-</code>). + * + * + * @version $Id: BodyDescriptor.java,v 1.4 2005/02/11 10:08:37 ntherning Exp $ + */ +public class BodyDescriptor { + private static Log log = LogFactory.getLog(BodyDescriptor.class); + + private String mimeType = "text/plain"; + private String boundary = null; + private String charset = "us-ascii"; + private String transferEncoding = "7bit"; + private Map<String, String> parameters = new HashMap<String, String>(); + private boolean contentTypeSet = false; + private boolean contentTransferEncSet = false; + + /** + * Creates a new root <code>BodyDescriptor</code> instance. + */ + public BodyDescriptor() { + this(null); + } + + /** + * Creates a new <code>BodyDescriptor</code> instance. + * + * @param parent the descriptor of the parent or <code>null</code> if this + * is the root descriptor. + */ + public BodyDescriptor(BodyDescriptor parent) { + if (parent != null && parent.isMimeType("multipart/digest")) { + mimeType = "message/rfc822"; + } else { + mimeType = "text/plain"; + } + } + + /** + * Should be called for each <code>Content-</code> header field of + * a MIME message or part. + * + * @param name the field name. + * @param value the field value. + */ + public void addField(String name, String value) { + + name = name.trim().toLowerCase(); + + if (name.equals("content-transfer-encoding") && !contentTransferEncSet) { + contentTransferEncSet = true; + + value = value.trim().toLowerCase(); + if (value.length() > 0) { + transferEncoding = value; + } + + } else if (name.equals("content-type") && !contentTypeSet) { + contentTypeSet = true; + + value = value.trim(); + + /* + * Unfold Content-Type value + */ + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '\r' || c == '\n') { + continue; + } + sb.append(c); + } + + Map<String, String> params = getHeaderParams(sb.toString()); + + String main = params.get(""); + if (main != null) { + main = main.toLowerCase().trim(); + int index = main.indexOf('/'); + boolean valid = false; + if (index != -1) { + String type = main.substring(0, index).trim(); + String subtype = main.substring(index + 1).trim(); + if (type.length() > 0 && subtype.length() > 0) { + main = type + "/" + subtype; + valid = true; + } + } + + if (!valid) { + main = null; + } + } + String b = params.get("boundary"); + + if (main != null + && ((main.startsWith("multipart/") && b != null) + || !main.startsWith("multipart/"))) { + + mimeType = main; + } + + if (isMultipart()) { + boundary = b; + } + + String c = params.get("charset"); + if (c != null) { + c = c.trim(); + if (c.length() > 0) { + charset = c.toLowerCase(); + } + } + + /* + * Add all other parameters to parameters. + */ + parameters.putAll(params); + parameters.remove(""); + parameters.remove("boundary"); + parameters.remove("charset"); + } + } + + private Map<String, String> getHeaderParams(String headerValue) { + Map<String, String> result = new HashMap<String, String>(); + + // split main value and parameters + String main; + String rest; + if (headerValue.indexOf(";") == -1) { + main = headerValue; + rest = null; + } else { + main = headerValue.substring(0, headerValue.indexOf(";")); + rest = headerValue.substring(main.length() + 1); + } + + result.put("", main); + if (rest != null) { + char[] chars = rest.toCharArray(); + StringBuffer paramName = new StringBuffer(); + StringBuffer paramValue = new StringBuffer(); + + final byte READY_FOR_NAME = 0; + final byte IN_NAME = 1; + final byte READY_FOR_VALUE = 2; + final byte IN_VALUE = 3; + final byte IN_QUOTED_VALUE = 4; + final byte VALUE_DONE = 5; + final byte ERROR = 99; + + byte state = READY_FOR_NAME; + boolean escaped = false; + for (int i = 0; i < chars.length; i++) { + char c = chars[i]; + + switch (state) { + case ERROR: + if (c == ';') + state = READY_FOR_NAME; + break; + + case READY_FOR_NAME: + if (c == '=') { + log.error("Expected header param name, got '='"); + state = ERROR; + break; + } + + paramName = new StringBuffer(); + paramValue = new StringBuffer(); + + state = IN_NAME; + // $FALL-THROUGH$ + + case IN_NAME: + if (c == '=') { + if (paramName.length() == 0) + state = ERROR; + else + state = READY_FOR_VALUE; + break; + } + + // not '='... just add to name + paramName.append(c); + break; + + case READY_FOR_VALUE: + boolean fallThrough = false; + switch (c) { + case ' ': + case '\t': + break; // ignore spaces, especially before '"' + + case '"': + state = IN_QUOTED_VALUE; + break; + + default: + state = IN_VALUE; + fallThrough = true; + break; + } + if (!fallThrough) + break; + + // $FALL-THROUGH$ + + case IN_VALUE: + fallThrough = false; + switch (c) { + case ';': + case ' ': + case '\t': + result.put( + paramName.toString().trim().toLowerCase(), + paramValue.toString().trim()); + state = VALUE_DONE; + fallThrough = true; + break; + default: + paramValue.append(c); + break; + } + if (!fallThrough) + break; + + // $FALL-THROUGH$ + + case VALUE_DONE: + switch (c) { + case ';': + state = READY_FOR_NAME; + break; + + case ' ': + case '\t': + break; + + default: + state = ERROR; + break; + } + break; + + case IN_QUOTED_VALUE: + switch (c) { + case '"': + if (!escaped) { + // don't trim quoted strings; the spaces could be intentional. + result.put( + paramName.toString().trim().toLowerCase(), + paramValue.toString()); + state = VALUE_DONE; + } else { + escaped = false; + paramValue.append(c); + } + break; + + case '\\': + if (escaped) { + paramValue.append('\\'); + } + escaped = !escaped; + break; + + default: + if (escaped) { + paramValue.append('\\'); + } + escaped = false; + paramValue.append(c); + break; + } + break; + + } + } + + // done looping. check if anything is left over. + if (state == IN_VALUE) { + result.put( + paramName.toString().trim().toLowerCase(), + paramValue.toString().trim()); + } + } + + return result; + } + + + public boolean isMimeType(String mimeType) { + return this.mimeType.equals(mimeType.toLowerCase()); + } + + /** + * Return true if the BodyDescriptor belongs to a message + */ + public boolean isMessage() { + return mimeType.equals("message/rfc822"); + } + + /** + * Return true if the BodyDescripotro belongs to a multipart + */ + public boolean isMultipart() { + return mimeType.startsWith("multipart/"); + } + + /** + * Return the MimeType + */ + public String getMimeType() { + return mimeType; + } + + /** + * Return the boundary + */ + public String getBoundary() { + return boundary; + } + + /** + * Return the charset + */ + public String getCharset() { + return charset; + } + + /** + * Return all parameters for the BodyDescriptor + */ + public Map<String, String> getParameters() { + return parameters; + } + + /** + * Return the TransferEncoding + */ + public String getTransferEncoding() { + return transferEncoding; + } + + /** + * Return true if it's base64 encoded + */ + public boolean isBase64Encoded() { + return "base64".equals(transferEncoding); + } + + /** + * Return true if it's quoted-printable + */ + public boolean isQuotedPrintableEncoded() { + return "quoted-printable".equals(transferEncoding); + } + + @Override + public String toString() { + return mimeType; + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java new file mode 100644 index 000000000..d9f3b078a --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java @@ -0,0 +1,129 @@ +/**************************************************************** + * 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 org.apache.james.mime4j; + +import java.io.InputStream; +import java.io.IOException; + +/** + * InputStream that shields its underlying input stream from + * being closed. + * + * + * @version $Id: CloseShieldInputStream.java,v 1.2 2004/10/02 12:41:10 ntherning Exp $ + */ +public class CloseShieldInputStream extends InputStream { + + /** + * Underlying InputStream + */ + private InputStream is; + + public CloseShieldInputStream(InputStream is) { + this.is = is; + } + + public InputStream getUnderlyingStream() { + return is; + } + + /** + * @see java.io.InputStream#read() + */ + public int read() throws IOException { + checkIfClosed(); + return is.read(); + } + + /** + * @see java.io.InputStream#available() + */ + public int available() throws IOException { + checkIfClosed(); + return is.available(); + } + + + /** + * Set the underlying InputStream to null + */ + public void close() throws IOException { + is = null; + } + + /** + * @see java.io.FilterInputStream#reset() + */ + public synchronized void reset() throws IOException { + checkIfClosed(); + is.reset(); + } + + /** + * @see java.io.FilterInputStream#markSupported() + */ + public boolean markSupported() { + if (is == null) + return false; + return is.markSupported(); + } + + /** + * @see java.io.FilterInputStream#mark(int) + */ + public synchronized void mark(int readlimit) { + if (is != null) + is.mark(readlimit); + } + + /** + * @see java.io.FilterInputStream#skip(long) + */ + public long skip(long n) throws IOException { + checkIfClosed(); + return is.skip(n); + } + + /** + * @see java.io.FilterInputStream#read(byte[]) + */ + public int read(byte b[]) throws IOException { + checkIfClosed(); + return is.read(b); + } + + /** + * @see java.io.FilterInputStream#read(byte[], int, int) + */ + public int read(byte b[], int off, int len) throws IOException { + checkIfClosed(); + return is.read(b, off, len); + } + + /** + * Check if the underlying InputStream is null. If so throw an Exception + * + * @throws IOException if the underlying InputStream is null + */ + private void checkIfClosed() throws IOException { + if (is == null) + throw new IOException("Stream is closed"); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java new file mode 100644 index 000000000..b437e739e --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java @@ -0,0 +1,177 @@ +/**************************************************************** + * 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 org.apache.james.mime4j; + +import java.io.IOException; +import java.io.InputStream; + +/** + * <p> + * Receives notifications of the content of a plain RFC822 or MIME message. + * Implement this interface and register an instance of that implementation + * with a <code>MimeStreamParser</code> instance using its + * {@link org.apache.james.mime4j.MimeStreamParser#setContentHandler(ContentHandler)} + * method. The parser uses the <code>ContentHandler</code> instance to report + * basic message-related events like the start and end of the body of a + * part in a multipart MIME entity. + * </p> + * <p> + * Events will be generated in the order the corresponding elements occur in + * the message stream parsed by the parser. E.g.: + * <pre> + * startMessage() + * startHeader() + * field(...) + * field(...) + * ... + * endHeader() + * startMultipart() + * preamble(...) + * startBodyPart() + * startHeader() + * field(...) + * field(...) + * ... + * endHeader() + * body() + * endBodyPart() + * startBodyPart() + * startHeader() + * field(...) + * field(...) + * ... + * endHeader() + * body() + * endBodyPart() + * epilogue(...) + * endMultipart() + * endMessage() + * </pre> + * The above shows an example of a MIME message consisting of a multipart + * body containing two body parts. + * </p> + * <p> + * See MIME RFCs 2045-2049 for more information on the structure of MIME + * messages and RFC 822 and 2822 for the general structure of Internet mail + * messages. + * </p> + * + * + * @version $Id: ContentHandler.java,v 1.3 2004/10/02 12:41:10 ntherning Exp $ + */ +public interface ContentHandler { + /** + * Called when a new message starts (a top level message or an embedded + * rfc822 message). + */ + void startMessage(); + + /** + * Called when a message ends. + */ + void endMessage(); + + /** + * Called when a new body part starts inside a + * <code>multipart/*</code> entity. + */ + void startBodyPart(); + + /** + * Called when a body part ends. + */ + void endBodyPart(); + + /** + * Called when a header (of a message or body part) is about to be parsed. + */ + void startHeader(); + + /** + * Called for each field of a header. + * + * @param fieldData the raw contents of the field + * (<code>Field-Name: field value</code>). The value will not be + * unfolded. + */ + void field(String fieldData); + + /** + * Called when there are no more header fields in a message or body part. + */ + void endHeader(); + + /** + * Called for the preamble (whatever comes before the first body part) + * of a <code>multipart/*</code> entity. + * + * @param is used to get the contents of the preamble. + * @throws IOException should be thrown on I/O errors. + */ + void preamble(InputStream is) throws IOException; + + /** + * Called for the epilogue (whatever comes after the final body part) + * of a <code>multipart/*</code> entity. + * + * @param is used to get the contents of the epilogue. + * @throws IOException should be thrown on I/O errors. + */ + void epilogue(InputStream is) throws IOException; + + /** + * Called when the body of a multipart entity is about to be parsed. + * + * @param bd encapsulates the values (either read from the + * message stream or, if not present, determined implictly + * as described in the + * MIME rfc:s) of the <code>Content-Type</code> and + * <code>Content-Transfer-Encoding</code> header fields. + */ + void startMultipart(BodyDescriptor bd); + + /** + * Called when the body of an entity has been parsed. + */ + void endMultipart(); + + /** + * Called when the body of a discrete (non-multipart) entity is about to + * be parsed. + * + * @param bd see {@link #startMultipart(BodyDescriptor)} + * @param is the contents of the body. NOTE: this is the raw body contents + * - it will not be decoded if encoded. The <code>bd</code> + * parameter should be used to determine how the stream data + * should be decoded. + * @throws IOException should be thrown on I/O errors. + */ + void body(BodyDescriptor bd, InputStream is) throws IOException; + + /** + * Called when a new entity (message or body part) starts and the + * parser is in <code>raw</code> mode. + * + * @param is the raw contents of the entity. + * @throws IOException should be thrown on I/O errors. + * @see MimeStreamParser#setRaw(boolean) + */ + void raw(InputStream is) throws IOException; +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java new file mode 100644 index 000000000..d6ef706b2 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java @@ -0,0 +1,139 @@ +/**************************************************************** + * 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 org.apache.james.mime4j; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; + +/** + * InputStream which converts <code>\r</code> + * bytes not followed by <code>\n</code> and <code>\n</code> not + * preceded by <code>\r</code> to <code>\r\n</code>. + * + * + * @version $Id: EOLConvertingInputStream.java,v 1.4 2004/11/29 13:15:42 ntherning Exp $ + */ +public class EOLConvertingInputStream extends InputStream { + /** Converts single '\r' to '\r\n' */ + public static final int CONVERT_CR = 1; + /** Converts single '\n' to '\r\n' */ + public static final int CONVERT_LF = 2; + /** Converts single '\r' and '\n' to '\r\n' */ + public static final int CONVERT_BOTH = 3; + + private PushbackInputStream in = null; + private int previous = 0; + private int flags = CONVERT_BOTH; + private int size = 0; + private int pos = 0; + private int nextTenPctPos; + private int tenPctSize; + private Callback callback; + + public interface Callback { + public void report(int bytesRead); + } + + /** + * Creates a new <code>EOLConvertingInputStream</code> + * instance converting bytes in the given <code>InputStream</code>. + * The flag <code>CONVERT_BOTH</code> is the default. + * + * @param in the <code>InputStream</code> to read from. + */ + public EOLConvertingInputStream(InputStream _in) { + super(); + in = new PushbackInputStream(_in, 2); + } + + /** + * Creates a new <code>EOLConvertingInputStream</code> + * instance converting bytes in the given <code>InputStream</code>. + * + * @param _in the <code>InputStream</code> to read from. + * @param _size the size of the input stream (need not be exact) + * @param _callback a callback reporting when each 10% of stream's size is reached + */ + public EOLConvertingInputStream(InputStream _in, int _size, Callback _callback) { + this(_in); + size = _size; + tenPctSize = size / 10; + nextTenPctPos = tenPctSize; + callback = _callback; + } + + /** + * Closes the underlying stream. + * + * @throws IOException on I/O errors. + */ + public void close() throws IOException { + in.close(); + } + + private int readByte() throws IOException { + int b = in.read(); + if (b != -1) { + if (callback != null && pos++ == nextTenPctPos) { + nextTenPctPos += tenPctSize; + if (callback != null) { + callback.report(pos); + } + } + } + return b; + } + + private void unreadByte(int c) throws IOException { + in.unread(c); + pos--; + } + + /** + * @see java.io.InputStream#read() + */ + public int read() throws IOException { + int b = readByte(); + + if (b == -1) { + pos = size; + return -1; + } + + if ((flags & CONVERT_CR) != 0 && b == '\r') { + int c = readByte(); + if (c != -1) { + unreadByte(c); + } + if (c != '\n') { + unreadByte('\n'); + } + } else if ((flags & CONVERT_LF) != 0 && b == '\n' && previous != '\r') { + b = '\r'; + unreadByte('\n'); + } + + previous = b; + + return b; + } + +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java new file mode 100644 index 000000000..5eeead5f3 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java @@ -0,0 +1,114 @@ +/* + * Copyright (C) 2009 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 org.apache.james.mime4j; + +/** + * Empty stub for the apache logging library. + */ +public class Log { + private static final String LOG_TAG = "Email Log"; + + public Log(Class mClazz) { + } + + public boolean isDebugEnabled() { + return false; + } + + public boolean isErrorEnabled() { + return true; + } + + public boolean isFatalEnabled() { + return true; + } + + public boolean isInfoEnabled() { + return false; + } + + public boolean isTraceEnabled() { + return false; + } + + public boolean isWarnEnabled() { + return true; + } + + public void trace(Object message) { + if (!isTraceEnabled()) return; + android.util.Log.v(LOG_TAG, toString(message, null)); + } + + public void trace(Object message, Throwable t) { + if (!isTraceEnabled()) return; + android.util.Log.v(LOG_TAG, toString(message, t)); + } + + public void debug(Object message) { + if (!isDebugEnabled()) return; + android.util.Log.d(LOG_TAG, toString(message, null)); + } + + public void debug(Object message, Throwable t) { + if (!isDebugEnabled()) return; + android.util.Log.d(LOG_TAG, toString(message, t)); + } + + public void info(Object message) { + if (!isInfoEnabled()) return; + android.util.Log.i(LOG_TAG, toString(message, null)); + } + + public void info(Object message, Throwable t) { + if (!isInfoEnabled()) return; + android.util.Log.i(LOG_TAG, toString(message, t)); + } + + public void warn(Object message) { + android.util.Log.w(LOG_TAG, toString(message, null)); + } + + public void warn(Object message, Throwable t) { + android.util.Log.w(LOG_TAG, toString(message, t)); + } + + public void error(Object message) { + android.util.Log.e(LOG_TAG, toString(message, null)); + } + + public void error(Object message, Throwable t) { + android.util.Log.e(LOG_TAG, toString(message, t)); + } + + public void fatal(Object message) { + android.util.Log.e(LOG_TAG, toString(message, null)); + } + + public void fatal(Object message, Throwable t) { + android.util.Log.e(LOG_TAG, toString(message, t)); + } + + private static String toString(Object o, Throwable t) { + String m = (o == null) ? "(null)" : o.toString(); + if (t == null) { + return m; + } else { + return m + " " + t.getMessage(); + } + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java new file mode 100644 index 000000000..ed6e3de3d --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2009 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 org.apache.james.mime4j; + +/** + * Empty stub for the apache logging library. + */ +public final class LogFactory { + private LogFactory() { + } + + public static Log getLog(Class clazz) { + return new Log(clazz); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java new file mode 100644 index 000000000..c6d6f248a --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java @@ -0,0 +1,184 @@ +/**************************************************************** + * 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 org.apache.james.mime4j; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; + +/** + * Stream that constrains itself to a single MIME body part. + * After the stream ends (i.e. read() returns -1) {@link #hasMoreParts()} + * can be used to determine if a final boundary has been seen or not. + * If {@link #parentEOF()} is <code>true</code> an unexpected end of stream + * has been detected in the parent stream. + * + * + * + * @version $Id: MimeBoundaryInputStream.java,v 1.2 2004/11/29 13:15:42 ntherning Exp $ + */ +public class MimeBoundaryInputStream extends InputStream { + + private PushbackInputStream s = null; + private byte[] boundary = null; + private boolean first = true; + private boolean eof = false; + private boolean parenteof = false; + private boolean moreParts = true; + + /** + * Creates a new MimeBoundaryInputStream. + * @param s The underlying stream. + * @param boundary Boundary string (not including leading hyphens). + */ + public MimeBoundaryInputStream(InputStream s, String boundary) + throws IOException { + + this.s = new PushbackInputStream(s, boundary.length() + 4); + + boundary = "--" + boundary; + this.boundary = new byte[boundary.length()]; + for (int i = 0; i < this.boundary.length; i++) { + this.boundary[i] = (byte) boundary.charAt(i); + } + + /* + * By reading one byte we will update moreParts to be as expected + * before any bytes have been read. + */ + int b = read(); + if (b != -1) { + this.s.unread(b); + } + } + + /** + * Closes the underlying stream. + * + * @throws IOException on I/O errors. + */ + public void close() throws IOException { + s.close(); + } + + /** + * Determines if the underlying stream has more parts (this stream has + * not seen an end boundary). + * + * @return <code>true</code> if there are more parts in the underlying + * stream, <code>false</code> otherwise. + */ + public boolean hasMoreParts() { + return moreParts; + } + + /** + * Determines if the parent stream has reached EOF + * + * @return <code>true</code> if EOF has been reached for the parent stream, + * <code>false</code> otherwise. + */ + public boolean parentEOF() { + return parenteof; + } + + /** + * Consumes all unread bytes of this stream. After a call to this method + * this stream will have reached EOF. + * + * @throws IOException on I/O errors. + */ + public void consume() throws IOException { + while (read() != -1) { + } + } + + /** + * @see java.io.InputStream#read() + */ + public int read() throws IOException { + if (eof) { + return -1; + } + + if (first) { + first = false; + if (matchBoundary()) { + return -1; + } + } + + int b1 = s.read(); + int b2 = s.read(); + + if (b1 == '\r' && b2 == '\n') { + if (matchBoundary()) { + return -1; + } + } + + if (b2 != -1) { + s.unread(b2); + } + + parenteof = b1 == -1; + eof = parenteof; + + return b1; + } + + private boolean matchBoundary() throws IOException { + + for (int i = 0; i < boundary.length; i++) { + int b = s.read(); + if (b != boundary[i]) { + if (b != -1) { + s.unread(b); + } + for (int j = i - 1; j >= 0; j--) { + s.unread(boundary[j]); + } + return false; + } + } + + /* + * We have a match. Is it an end boundary? + */ + int prev = s.read(); + int curr = s.read(); + moreParts = !(prev == '-' && curr == '-'); + do { + if (curr == '\n' && prev == '\r') { + break; + } + prev = curr; + } while ((curr = s.read()) != -1); + + if (curr == -1) { + moreParts = false; + parenteof = true; + } + + eof = true; + + return true; + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java new file mode 100644 index 000000000..a8aad5a38 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java @@ -0,0 +1,324 @@ +/**************************************************************** + * 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 org.apache.james.mime4j; + +import org.apache.james.mime4j.decoder.Base64InputStream; +import org.apache.james.mime4j.decoder.QuotedPrintableInputStream; + +import java.io.IOException; +import java.io.InputStream; +import java.util.BitSet; +import java.util.LinkedList; + +/** + * <p> + * Parses MIME (or RFC822) message streams of bytes or characters and reports + * parsing events to a <code>ContentHandler</code> instance. + * </p> + * <p> + * Typical usage:<br/> + * <pre> + * ContentHandler handler = new MyHandler(); + * MimeStreamParser parser = new MimeStreamParser(); + * parser.setContentHandler(handler); + * parser.parse(new BufferedInputStream(new FileInputStream("mime.msg"))); + * </pre> + * <strong>NOTE:</strong> All lines must end with CRLF + * (<code>\r\n</code>). If you are unsure of the line endings in your stream + * you should wrap it in a {@link org.apache.james.mime4j.EOLConvertingInputStream} instance. + * + * + * @version $Id: MimeStreamParser.java,v 1.8 2005/02/11 10:12:02 ntherning Exp $ + */ +public class MimeStreamParser { + private static final Log log = LogFactory.getLog(MimeStreamParser.class); + + private static BitSet fieldChars = null; + + private RootInputStream rootStream = null; + private LinkedList<BodyDescriptor> bodyDescriptors = new LinkedList<BodyDescriptor>(); + private ContentHandler handler = null; + private boolean raw = false; + private boolean prematureEof = false; + + static { + fieldChars = new BitSet(); + for (int i = 0x21; i <= 0x39; i++) { + fieldChars.set(i); + } + for (int i = 0x3b; i <= 0x7e; i++) { + fieldChars.set(i); + } + } + + /** + * Creates a new <code>MimeStreamParser</code> instance. + */ + public MimeStreamParser() { + } + + /** + * Parses a stream of bytes containing a MIME message. + * + * @param is the stream to parse. + * @throws IOException on I/O errors. + */ + public void parse(InputStream is) throws IOException { + rootStream = new RootInputStream(is); + parseMessage(rootStream); + } + + /** + * Determines if this parser is currently in raw mode. + * + * @return <code>true</code> if in raw mode, <code>false</code> + * otherwise. + * @see #setRaw(boolean) + */ + public boolean isRaw() { + return raw; + } + + /** + * Enables or disables raw mode. In raw mode all future entities + * (messages or body parts) in the stream will be reported to the + * {@link ContentHandler#raw(InputStream)} handler method only. + * The stream will contain the entire unparsed entity contents + * including header fields and whatever is in the body. + * + * @param raw <code>true</code> enables raw mode, <code>false</code> + * disables it. + */ + public void setRaw(boolean raw) { + this.raw = raw; + } + + /** + * Finishes the parsing and stops reading lines. + * NOTE: No more lines will be parsed but the parser + * will still call + * {@link ContentHandler#endMultipart()}, + * {@link ContentHandler#endBodyPart()}, + * {@link ContentHandler#endMessage()}, etc to match previous calls + * to + * {@link ContentHandler#startMultipart(BodyDescriptor)}, + * {@link ContentHandler#startBodyPart()}, + * {@link ContentHandler#startMessage()}, etc. + */ + public void stop() { + rootStream.truncate(); + } + + /** + * Parses an entity which consists of a header followed by a body containing + * arbitrary data, body parts or an embedded message. + * + * @param is the stream to parse. + * @throws IOException on I/O errors. + */ + private void parseEntity(InputStream is) throws IOException { + BodyDescriptor bd = parseHeader(is); + + if (bd.isMultipart()) { + bodyDescriptors.addFirst(bd); + + handler.startMultipart(bd); + + MimeBoundaryInputStream tempIs = + new MimeBoundaryInputStream(is, bd.getBoundary()); + handler.preamble(new CloseShieldInputStream(tempIs)); + tempIs.consume(); + + while (tempIs.hasMoreParts()) { + tempIs = new MimeBoundaryInputStream(is, bd.getBoundary()); + parseBodyPart(tempIs); + tempIs.consume(); + if (tempIs.parentEOF()) { + prematureEof = true; +// if (log.isWarnEnabled()) { +// log.warn("Line " + rootStream.getLineNumber() +// + ": Body part ended prematurely. " +// + "Higher level boundary detected or " +// + "EOF reached."); +// } + break; + } + } + + handler.epilogue(new CloseShieldInputStream(is)); + + handler.endMultipart(); + + bodyDescriptors.removeFirst(); + + } else if (bd.isMessage()) { + if (bd.isBase64Encoded()) { + log.warn("base64 encoded message/rfc822 detected"); + is = new EOLConvertingInputStream( + new Base64InputStream(is)); + } else if (bd.isQuotedPrintableEncoded()) { + log.warn("quoted-printable encoded message/rfc822 detected"); + is = new EOLConvertingInputStream( + new QuotedPrintableInputStream(is)); + } + bodyDescriptors.addFirst(bd); + parseMessage(is); + bodyDescriptors.removeFirst(); + } else { + handler.body(bd, new CloseShieldInputStream(is)); + } + + /* + * Make sure the stream has been consumed. + */ + while (is.read() != -1) { + } + } + + private void parseMessage(InputStream is) throws IOException { + if (raw) { + handler.raw(new CloseShieldInputStream(is)); + } else { + handler.startMessage(); + parseEntity(is); + handler.endMessage(); + } + } + + public boolean getPrematureEof() { + return prematureEof; + } + + private void parseBodyPart(InputStream is) throws IOException { + if (raw) { + handler.raw(new CloseShieldInputStream(is)); + } else { + handler.startBodyPart(); + parseEntity(is); + handler.endBodyPart(); + } + } + + /** + * Parses a header. + * + * @param is the stream to parse. + * @return a <code>BodyDescriptor</code> describing the body following + * the header. + */ + private BodyDescriptor parseHeader(InputStream is) throws IOException { + BodyDescriptor bd = new BodyDescriptor(bodyDescriptors.isEmpty() + ? null : (BodyDescriptor) bodyDescriptors.getFirst()); + + handler.startHeader(); + + int lineNumber = rootStream.getLineNumber(); + + StringBuffer sb = new StringBuffer(); + int curr = 0; + int prev = 0; + while ((curr = is.read()) != -1) { + if (curr == '\n' && (prev == '\n' || prev == 0)) { + /* + * [\r]\n[\r]\n or an immediate \r\n have been seen. + */ + sb.deleteCharAt(sb.length() - 1); + break; + } + sb.append((char) curr); + prev = curr == '\r' ? prev : curr; + } + +// if (curr == -1 && log.isWarnEnabled()) { +// log.warn("Line " + rootStream.getLineNumber() +// + ": Unexpected end of headers detected. " +// + "Boundary detected in header or EOF reached."); +// } + + int start = 0; + int pos = 0; + int startLineNumber = lineNumber; + while (pos < sb.length()) { + while (pos < sb.length() && sb.charAt(pos) != '\r') { + pos++; + } + if (pos < sb.length() - 1 && sb.charAt(pos + 1) != '\n') { + pos++; + continue; + } + + if (pos >= sb.length() - 2 || fieldChars.get(sb.charAt(pos + 2))) { + + /* + * field should be the complete field data excluding the + * trailing \r\n. + */ + String field = sb.substring(start, pos); + start = pos + 2; + + /* + * Check for a valid field. + */ + int index = field.indexOf(':'); + boolean valid = false; + if (index != -1 && fieldChars.get(field.charAt(0))) { + valid = true; + String fieldName = field.substring(0, index).trim(); + for (int i = 0; i < fieldName.length(); i++) { + if (!fieldChars.get(fieldName.charAt(i))) { + valid = false; + break; + } + } + + if (valid) { + handler.field(field); + bd.addField(fieldName, field.substring(index + 1)); + } + } + + if (!valid && log.isWarnEnabled()) { + log.warn("Line " + startLineNumber + + ": Ignoring invalid field: '" + field.trim() + "'"); + } + + startLineNumber = lineNumber; + } + + pos += 2; + lineNumber++; + } + + handler.endHeader(); + + return bd; + } + + /** + * Sets the <code>ContentHandler</code> to use when reporting + * parsing events. + * + * @param h the <code>ContentHandler</code>. + */ + public void setContentHandler(ContentHandler h) { + this.handler = h; + } + +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java new file mode 100644 index 000000000..cc8b2411c --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java @@ -0,0 +1,111 @@ +/**************************************************************** + * 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 org.apache.james.mime4j; + +import java.io.IOException; +import java.io.InputStream; + +/** + * <code>InputStream</code> used by the parser to wrap the original user + * supplied stream. This stream keeps track of the current line number and + * can also be truncated. When truncated the stream will appear to have + * reached end of file. This is used by the parser's + * {@link org.apache.james.mime4j.MimeStreamParser#stop()} method. + * + * + * @version $Id: RootInputStream.java,v 1.2 2004/10/02 12:41:10 ntherning Exp $ + */ +class RootInputStream extends InputStream { + private InputStream is = null; + private int lineNumber = 1; + private int prev = -1; + private boolean truncated = false; + + /** + * Creates a new <code>RootInputStream</code>. + * + * @param in the stream to read from. + */ + public RootInputStream(InputStream is) { + this.is = is; + } + + /** + * Gets the current line number starting at 1 + * (the number of <code>\r\n</code> read so far plus 1). + * + * @return the current line number. + */ + public int getLineNumber() { + return lineNumber; + } + + /** + * Truncates this <code>InputStream</code>. After this call any + * call to {@link #read()}, {@link #read(byte[]) or + * {@link #read(byte[], int, int)} will return + * -1 as if end-of-file had been reached. + */ + public void truncate() { + this.truncated = true; + } + + /** + * @see java.io.InputStream#read() + */ + public int read() throws IOException { + if (truncated) { + return -1; + } + + int b = is.read(); + if (prev == '\r' && b == '\n') { + lineNumber++; + } + prev = b; + return b; + } + + /** + * + * @see java.io.InputStream#read(byte[], int, int) + */ + public int read(byte[] b, int off, int len) throws IOException { + if (truncated) { + return -1; + } + + int n = is.read(b, off, len); + for (int i = off; i < off + n; i++) { + if (prev == '\r' && b[i] == '\n') { + lineNumber++; + } + prev = b[i]; + } + return n; + } + + /** + * @see java.io.InputStream#read(byte[]) + */ + public int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java new file mode 100644 index 000000000..6841bc998 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java @@ -0,0 +1,630 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.codec; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.BitSet; +import java.util.Locale; + +import org.apache.james.mime4j.util.CharsetUtil; + +/** + * ANDROID: THIS CLASS IS COPIED FROM A NEWER VERSION OF MIME4J + */ + +/** + * Static methods for encoding header field values. This includes encoded-words + * as defined in <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC 2047</a> + * or display-names of an e-mail address, for example. + * + */ +public class EncoderUtil { + + // 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. + // ANDROID: THIS TABLE IS COPIED FROM BASE64OUTPUTSTREAM + static final byte[] BASE64_TABLE = { '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 BASE64_PAD = '='; + + private static final BitSet Q_REGULAR_CHARS = initChars("=_?"); + + private static final BitSet Q_RESTRICTED_CHARS = initChars("=_?\"#$%&'(),.:;<>@[\\]^`{|}~"); + + private static final int MAX_USED_CHARACTERS = 50; + + private static final String ENC_WORD_PREFIX = "=?"; + private static final String ENC_WORD_SUFFIX = "?="; + + private static final int ENCODED_WORD_MAX_LENGTH = 75; // RFC 2047 + + private static final BitSet TOKEN_CHARS = initChars("()<>@,;:\\\"/[]?="); + + private static final BitSet ATEXT_CHARS = initChars("()<>@.,;:\\\"[]"); + + private static BitSet initChars(String specials) { + BitSet bs = new BitSet(128); + for (char ch = 33; ch < 127; ch++) { + if (specials.indexOf(ch) == -1) { + bs.set(ch); + } + } + return bs; + } + + /** + * Selects one of the two encodings specified in RFC 2047. + */ + public enum Encoding { + /** The B encoding (identical to base64 defined in RFC 2045). */ + B, + /** The Q encoding (similar to quoted-printable defined in RFC 2045). */ + Q + } + + /** + * Indicates the intended usage of an encoded word. + */ + public enum Usage { + /** + * Encoded word is used to replace a 'text' token in any Subject or + * Comments header field. + */ + TEXT_TOKEN, + /** + * Encoded word is used to replace a 'word' entity within a 'phrase', + * for example, one that precedes an address in a From, To, or Cc + * header. + */ + WORD_ENTITY + } + + private EncoderUtil() { + } + + /** + * Encodes the display-name portion of an address. See <a + * href='http://www.faqs.org/rfcs/rfc5322.html'>RFC 5322</a> section 3.4 + * and <a href='http://www.faqs.org/rfcs/rfc2047.html'>RFC 2047</a> section + * 5.3. The specified string should not be folded. + * + * @param displayName + * display-name to encode. + * @return encoded display-name. + */ + public static String encodeAddressDisplayName(String displayName) { + // display-name = phrase + // phrase = 1*( encoded-word / word ) + // word = atom / quoted-string + // atom = [CFWS] 1*atext [CFWS] + // CFWS = comment or folding white space + + if (isAtomPhrase(displayName)) { + return displayName; + } else if (hasToBeEncoded(displayName, 0)) { + return encodeEncodedWord(displayName, Usage.WORD_ENTITY); + } else { + return quote(displayName); + } + } + + /** + * Encodes the local part of an address specification as described in RFC + * 5322 section 3.4.1. Leading and trailing CFWS should have been removed + * before calling this method. The specified string should not contain any + * illegal (control or non-ASCII) characters. + * + * @param localPart + * the local part to encode + * @return the encoded local part. + */ + public static String encodeAddressLocalPart(String localPart) { + // local-part = dot-atom / quoted-string + // dot-atom = [CFWS] dot-atom-text [CFWS] + // CFWS = comment or folding white space + + if (isDotAtomText(localPart)) { + return localPart; + } else { + return quote(localPart); + } + } + + /** + * Encodes the specified strings into a header parameter as described in RFC + * 2045 section 5.1 and RFC 2183 section 2. The specified strings should not + * contain any illegal (control or non-ASCII) characters. + * + * @param name + * parameter name. + * @param value + * parameter value. + * @return encoded result. + */ + public static String encodeHeaderParameter(String name, String value) { + name = name.toLowerCase(Locale.US); + + // value := token / quoted-string + if (isToken(value)) { + return name + "=" + value; + } else { + return name + "=" + quote(value); + } + } + + /** + * Shortcut method that encodes the specified text into an encoded-word if + * the text has to be encoded. + * + * @param text + * text to encode. + * @param usage + * whether the encoded-word is to be used to replace a text token + * or a word entity (see RFC 822). + * @param usedCharacters + * number of characters already used up (<code>0 <= usedCharacters <= 50</code>). + * @return the specified text if encoding is not necessary or an encoded + * word or a sequence of encoded words otherwise. + */ + public static String encodeIfNecessary(String text, Usage usage, + int usedCharacters) { + if (hasToBeEncoded(text, usedCharacters)) + return encodeEncodedWord(text, usage, usedCharacters); + else + return text; + } + + /** + * Determines if the specified string has to encoded into an encoded-word. + * Returns <code>true</code> if the text contains characters that don't + * fall into the printable ASCII character set or if the text contains a + * 'word' (sequence of non-whitespace characters) longer than 77 characters + * (including characters already used up in the line). + * + * @param text + * text to analyze. + * @param usedCharacters + * number of characters already used up (<code>0 <= usedCharacters <= 50</code>). + * @return <code>true</code> if the specified text has to be encoded into + * an encoded-word, <code>false</code> otherwise. + */ + public static boolean hasToBeEncoded(String text, int usedCharacters) { + if (text == null) + throw new IllegalArgumentException(); + if (usedCharacters < 0 || usedCharacters > MAX_USED_CHARACTERS) + throw new IllegalArgumentException(); + + int nonWhiteSpaceCount = usedCharacters; + + for (int idx = 0; idx < text.length(); idx++) { + char ch = text.charAt(idx); + if (ch == '\t' || ch == ' ') { + nonWhiteSpaceCount = 0; + } else { + nonWhiteSpaceCount++; + if (nonWhiteSpaceCount > 77) { + // Line cannot be folded into multiple lines with no more + // than 78 characters each. Encoding as encoded-words makes + // that possible. One character has to be reserved for + // folding white space; that leaves 77 characters. + return true; + } + + if (ch < 32 || ch >= 127) { + // non-printable ascii character has to be encoded + return true; + } + } + } + + return false; + } + + /** + * Encodes the specified text into an encoded word or a sequence of encoded + * words separated by space. The text is separated into a sequence of + * encoded words if it does not fit in a single one. + * <p> + * The charset to encode the specified text into a byte array and the + * encoding to use for the encoded-word are detected automatically. + * <p> + * This method assumes that zero characters have already been used up in the + * current line. + * + * @param text + * text to encode. + * @param usage + * whether the encoded-word is to be used to replace a text token + * or a word entity (see RFC 822). + * @return the encoded word (or sequence of encoded words if the given text + * does not fit in a single encoded word). + * @see #hasToBeEncoded(String, int) + */ + public static String encodeEncodedWord(String text, Usage usage) { + return encodeEncodedWord(text, usage, 0, null, null); + } + + /** + * Encodes the specified text into an encoded word or a sequence of encoded + * words separated by space. The text is separated into a sequence of + * encoded words if it does not fit in a single one. + * <p> + * The charset to encode the specified text into a byte array and the + * encoding to use for the encoded-word are detected automatically. + * + * @param text + * text to encode. + * @param usage + * whether the encoded-word is to be used to replace a text token + * or a word entity (see RFC 822). + * @param usedCharacters + * number of characters already used up (<code>0 <= usedCharacters <= 50</code>). + * @return the encoded word (or sequence of encoded words if the given text + * does not fit in a single encoded word). + * @see #hasToBeEncoded(String, int) + */ + public static String encodeEncodedWord(String text, Usage usage, + int usedCharacters) { + return encodeEncodedWord(text, usage, usedCharacters, null, null); + } + + /** + * Encodes the specified text into an encoded word or a sequence of encoded + * words separated by space. The text is separated into a sequence of + * encoded words if it does not fit in a single one. + * + * @param text + * text to encode. + * @param usage + * whether the encoded-word is to be used to replace a text token + * or a word entity (see RFC 822). + * @param usedCharacters + * number of characters already used up (<code>0 <= usedCharacters <= 50</code>). + * @param charset + * the Java charset that should be used to encode the specified + * string into a byte array. A suitable charset is detected + * automatically if this parameter is <code>null</code>. + * @param encoding + * the encoding to use for the encoded-word (either B or Q). A + * suitable encoding is automatically chosen if this parameter is + * <code>null</code>. + * @return the encoded word (or sequence of encoded words if the given text + * does not fit in a single encoded word). + * @see #hasToBeEncoded(String, int) + */ + public static String encodeEncodedWord(String text, Usage usage, + int usedCharacters, Charset charset, Encoding encoding) { + if (text == null) + throw new IllegalArgumentException(); + if (usedCharacters < 0 || usedCharacters > MAX_USED_CHARACTERS) + throw new IllegalArgumentException(); + + if (charset == null) + charset = determineCharset(text); + + String mimeCharset = CharsetUtil.toMimeCharset(charset.name()); + if (mimeCharset == null) { + // cannot happen if charset was originally null + throw new IllegalArgumentException("Unsupported charset"); + } + + byte[] bytes = encode(text, charset); + + if (encoding == null) + encoding = determineEncoding(bytes, usage); + + if (encoding == Encoding.B) { + String prefix = ENC_WORD_PREFIX + mimeCharset + "?B?"; + return encodeB(prefix, text, usedCharacters, charset, bytes); + } else { + String prefix = ENC_WORD_PREFIX + mimeCharset + "?Q?"; + return encodeQ(prefix, text, usage, usedCharacters, charset, bytes); + } + } + + /** + * Encodes the specified byte array using the B encoding defined in RFC + * 2047. + * + * @param bytes + * byte array to encode. + * @return encoded string. + */ + public static String encodeB(byte[] bytes) { + StringBuilder sb = new StringBuilder(); + + int idx = 0; + final int end = bytes.length; + for (; idx < end - 2; idx += 3) { + int data = (bytes[idx] & 0xff) << 16 | (bytes[idx + 1] & 0xff) << 8 + | bytes[idx + 2] & 0xff; + sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]); + sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]); + sb.append((char) BASE64_TABLE[data >> 6 & 0x3f]); + sb.append((char) BASE64_TABLE[data & 0x3f]); + } + + if (idx == end - 2) { + int data = (bytes[idx] & 0xff) << 16 | (bytes[idx + 1] & 0xff) << 8; + sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]); + sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]); + sb.append((char) BASE64_TABLE[data >> 6 & 0x3f]); + sb.append((char) BASE64_PAD); + + } else if (idx == end - 1) { + int data = (bytes[idx] & 0xff) << 16; + sb.append((char) BASE64_TABLE[data >> 18 & 0x3f]); + sb.append((char) BASE64_TABLE[data >> 12 & 0x3f]); + sb.append((char) BASE64_PAD); + sb.append((char) BASE64_PAD); + } + + return sb.toString(); + } + + /** + * Encodes the specified byte array using the Q encoding defined in RFC + * 2047. + * + * @param bytes + * byte array to encode. + * @param usage + * whether the encoded-word is to be used to replace a text token + * or a word entity (see RFC 822). + * @return encoded string. + */ + public static String encodeQ(byte[] bytes, Usage usage) { + BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS + : Q_RESTRICTED_CHARS; + + StringBuilder sb = new StringBuilder(); + + final int end = bytes.length; + for (int idx = 0; idx < end; idx++) { + int v = bytes[idx] & 0xff; + if (v == 32) { + sb.append('_'); + } else if (!qChars.get(v)) { + sb.append('='); + sb.append(hexDigit(v >>> 4)); + sb.append(hexDigit(v & 0xf)); + } else { + sb.append((char) v); + } + } + + return sb.toString(); + } + + /** + * Tests whether the specified string is a token as defined in RFC 2045 + * section 5.1. + * + * @param str + * string to test. + * @return <code>true</code> if the specified string is a RFC 2045 token, + * <code>false</code> otherwise. + */ + public static boolean isToken(String str) { + // token := 1*<any (US-ASCII) CHAR except SPACE, CTLs, or tspecials> + // tspecials := "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" / + // <"> / "/" / "[" / "]" / "?" / "=" + // CTL := 0.- 31., 127. + + final int length = str.length(); + if (length == 0) + return false; + + for (int idx = 0; idx < length; idx++) { + char ch = str.charAt(idx); + if (!TOKEN_CHARS.get(ch)) + return false; + } + + return true; + } + + private static boolean isAtomPhrase(String str) { + // atom = [CFWS] 1*atext [CFWS] + + boolean containsAText = false; + + final int length = str.length(); + for (int idx = 0; idx < length; idx++) { + char ch = str.charAt(idx); + if (ATEXT_CHARS.get(ch)) { + containsAText = true; + } else if (!CharsetUtil.isWhitespace(ch)) { + return false; + } + } + + return containsAText; + } + + // RFC 5322 section 3.2.3 + private static boolean isDotAtomText(String str) { + // dot-atom-text = 1*atext *("." 1*atext) + // atext = ALPHA / DIGIT / "!" / "#" / "$" / "%" / "&" / "'" / "*" / + // "+" / "-" / "/" / "=" / "?" / "^" / "_" / "`" / "{" / "|" / "}" / "~" + + char prev = '.'; + + final int length = str.length(); + if (length == 0) + return false; + + for (int idx = 0; idx < length; idx++) { + char ch = str.charAt(idx); + + if (ch == '.') { + if (prev == '.' || idx == length - 1) + return false; + } else { + if (!ATEXT_CHARS.get(ch)) + return false; + } + + prev = ch; + } + + return true; + } + + // RFC 5322 section 3.2.4 + private static String quote(String str) { + // quoted-string = [CFWS] DQUOTE *([FWS] qcontent) [FWS] DQUOTE [CFWS] + // qcontent = qtext / quoted-pair + // qtext = %d33 / %d35-91 / %d93-126 + // quoted-pair = ("\" (VCHAR / WSP)) + // VCHAR = %x21-7E + // DQUOTE = %x22 + + String escaped = str.replaceAll("[\\\\\"]", "\\\\$0"); + return "\"" + escaped + "\""; + } + + private static String encodeB(String prefix, String text, + int usedCharacters, Charset charset, byte[] bytes) { + int encodedLength = bEncodedLength(bytes); + + int totalLength = prefix.length() + encodedLength + + ENC_WORD_SUFFIX.length(); + if (totalLength <= ENCODED_WORD_MAX_LENGTH - usedCharacters) { + return prefix + encodeB(bytes) + ENC_WORD_SUFFIX; + } else { + int splitOffset = text.offsetByCodePoints(text.length() / 2, -1); + + String part1 = text.substring(0, splitOffset); + byte[] bytes1 = encode(part1, charset); + String word1 = encodeB(prefix, part1, usedCharacters, charset, + bytes1); + + String part2 = text.substring(splitOffset); + byte[] bytes2 = encode(part2, charset); + String word2 = encodeB(prefix, part2, 0, charset, bytes2); + + return word1 + " " + word2; + } + } + + private static int bEncodedLength(byte[] bytes) { + return (bytes.length + 2) / 3 * 4; + } + + private static String encodeQ(String prefix, String text, Usage usage, + int usedCharacters, Charset charset, byte[] bytes) { + int encodedLength = qEncodedLength(bytes, usage); + + int totalLength = prefix.length() + encodedLength + + ENC_WORD_SUFFIX.length(); + if (totalLength <= ENCODED_WORD_MAX_LENGTH - usedCharacters) { + return prefix + encodeQ(bytes, usage) + ENC_WORD_SUFFIX; + } else { + int splitOffset = text.offsetByCodePoints(text.length() / 2, -1); + + String part1 = text.substring(0, splitOffset); + byte[] bytes1 = encode(part1, charset); + String word1 = encodeQ(prefix, part1, usage, usedCharacters, + charset, bytes1); + + String part2 = text.substring(splitOffset); + byte[] bytes2 = encode(part2, charset); + String word2 = encodeQ(prefix, part2, usage, 0, charset, bytes2); + + return word1 + " " + word2; + } + } + + private static int qEncodedLength(byte[] bytes, Usage usage) { + BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS + : Q_RESTRICTED_CHARS; + + int count = 0; + + for (int idx = 0; idx < bytes.length; idx++) { + int v = bytes[idx] & 0xff; + if (v == 32) { + count++; + } else if (!qChars.get(v)) { + count += 3; + } else { + count++; + } + } + + return count; + } + + private static byte[] encode(String text, Charset charset) { + ByteBuffer buffer = charset.encode(text); + byte[] bytes = new byte[buffer.limit()]; + buffer.get(bytes); + return bytes; + } + + private static Charset determineCharset(String text) { + // it is an important property of iso-8859-1 that it directly maps + // unicode code points 0000 to 00ff to byte values 00 to ff. + boolean ascii = true; + final int len = text.length(); + for (int index = 0; index < len; index++) { + char ch = text.charAt(index); + if (ch > 0xff) { + return CharsetUtil.UTF_8; + } + if (ch > 0x7f) { + ascii = false; + } + } + return ascii ? CharsetUtil.US_ASCII : CharsetUtil.ISO_8859_1; + } + + private static Encoding determineEncoding(byte[] bytes, Usage usage) { + if (bytes.length == 0) + return Encoding.Q; + + BitSet qChars = usage == Usage.TEXT_TOKEN ? Q_REGULAR_CHARS + : Q_RESTRICTED_CHARS; + + int qEncoded = 0; + for (int i = 0; i < bytes.length; i++) { + int v = bytes[i] & 0xff; + if (v != 32 && !qChars.get(v)) { + qEncoded++; + } + } + + int percentage = qEncoded * 100 / bytes.length; + return percentage > 30 ? Encoding.B : Encoding.Q; + } + + private static char hexDigit(int i) { + return i < 10 ? (char) (i + '0') : (char) (i - 10 + 'A'); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java new file mode 100644 index 000000000..77f5d7d4a --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java @@ -0,0 +1,151 @@ +/**************************************************************** + * 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. * + ****************************************************************/ + +/** + * Modified to improve efficiency by Android 21-Aug-2009 + */ + +package org.apache.james.mime4j.decoder; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Performs Base-64 decoding on an underlying stream. + * + * + * @version $Id: Base64InputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $ + */ +public class Base64InputStream extends InputStream { + private final InputStream s; + private int outCount = 0; + private int outIndex = 0; + private final int[] outputBuffer = new int[3]; + private final byte[] inputBuffer = new byte[4]; + private boolean done = false; + + public Base64InputStream(InputStream s) { + this.s = s; + } + + /** + * Closes the underlying stream. + * + * @throws IOException on I/O errors. + */ + @Override + public void close() throws IOException { + s.close(); + } + + @Override + public int read() throws IOException { + if (outIndex == outCount) { + fillBuffer(); + if (outIndex == outCount) { + return -1; + } + } + + return outputBuffer[outIndex++]; + } + + /** + * Retrieve data from the underlying stream, decode it, + * and put the results in the byteq. + * @throws IOException + */ + private void fillBuffer() throws IOException { + outCount = 0; + outIndex = 0; + int inCount = 0; + + int i; + // "done" is needed for the two successive '=' at the end + while (!done) { + switch (i = s.read()) { + case -1: + // No more input - just return, let outputBuffer drain out, and be done + return; + case '=': + // once we meet the first '=', avoid reading the second '=' + done = true; + decodeAndEnqueue(inCount); + return; + default: + byte sX = TRANSLATION[i]; + if (sX < 0) continue; + inputBuffer[inCount++] = sX; + if (inCount == 4) { + decodeAndEnqueue(inCount); + return; + } + break; + } + } + } + + private void decodeAndEnqueue(int len) { + int accum = 0; + accum |= inputBuffer[0] << 18; + accum |= inputBuffer[1] << 12; + accum |= inputBuffer[2] << 6; + accum |= inputBuffer[3]; + + // There's a bit of duplicated code here because we want to have straight-through operation + // for the most common case of len==4 + if (len == 4) { + outputBuffer[0] = (accum >> 16) & 0xFF; + outputBuffer[1] = (accum >> 8) & 0xFF; + outputBuffer[2] = (accum) & 0xFF; + outCount = 3; + return; + } else if (len == 3) { + outputBuffer[0] = (accum >> 16) & 0xFF; + outputBuffer[1] = (accum >> 8) & 0xFF; + outCount = 2; + return; + } else { // len == 2 + outputBuffer[0] = (accum >> 16) & 0xFF; + outCount = 1; + return; + } + } + + private static byte[] TRANSLATION = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x00 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x10 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, /* 0x20 */ + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, -1, -1, -1, /* 0x30 */ + -1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, /* 0x40 */ + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, -1, -1, -1, -1, -1, /* 0x50 */ + -1, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, /* 0x60 */ + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, -1, -1, -1, -1, -1, /* 0x70 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x80 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0x90 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xA0 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xB0 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xC0 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xD0 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, /* 0xE0 */ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 /* 0xF0 */ + }; + + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java new file mode 100644 index 000000000..6d7ccef52 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java @@ -0,0 +1,62 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.decoder; + +import java.util.Iterator; + +public class ByteQueue { + + private UnboundedFifoByteBuffer buf; + private int initialCapacity = -1; + + public ByteQueue() { + buf = new UnboundedFifoByteBuffer(); + } + + public ByteQueue(int initialCapacity) { + buf = new UnboundedFifoByteBuffer(initialCapacity); + this.initialCapacity = initialCapacity; + } + + public void enqueue(byte b) { + buf.add(b); + } + + public byte dequeue() { + return buf.remove(); + } + + public int count() { + return buf.size(); + } + + public void clear() { + if (initialCapacity != -1) + buf = new UnboundedFifoByteBuffer(initialCapacity); + else + buf = new UnboundedFifoByteBuffer(); + } + + public Iterator iterator() { + return buf.iterator(); + } + + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java new file mode 100644 index 000000000..48fe07dee --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java @@ -0,0 +1,284 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.decoder; + +//BEGIN android-changed: Stubbing out logging +import org.apache.james.mime4j.Log; +import org.apache.james.mime4j.LogFactory; +//END android-changed +import org.apache.james.mime4j.util.CharsetUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; + +/** + * Static methods for decoding strings, byte arrays and encoded words. + * + * + * @version $Id: DecoderUtil.java,v 1.3 2005/02/07 15:33:59 ntherning Exp $ + */ +public class DecoderUtil { + private static Log log = LogFactory.getLog(DecoderUtil.class); + + /** + * Decodes a string containing quoted-printable encoded data. + * + * @param s the string to decode. + * @return the decoded bytes. + */ + public static byte[] decodeBaseQuotedPrintable(String s) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + try { + byte[] bytes = s.getBytes("US-ASCII"); + + QuotedPrintableInputStream is = new QuotedPrintableInputStream( + new ByteArrayInputStream(bytes)); + + int b = 0; + while ((b = is.read()) != -1) { + baos.write(b); + } + } catch (IOException e) { + /* + * This should never happen! + */ + log.error(e); + } + + return baos.toByteArray(); + } + + /** + * Decodes a string containing base64 encoded data. + * + * @param s the string to decode. + * @return the decoded bytes. + */ + public static byte[] decodeBase64(String s) { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + try { + byte[] bytes = s.getBytes("US-ASCII"); + + Base64InputStream is = new Base64InputStream( + new ByteArrayInputStream(bytes)); + + int b = 0; + while ((b = is.read()) != -1) { + baos.write(b); + } + } catch (IOException e) { + /* + * This should never happen! + */ + log.error(e); + } + + return baos.toByteArray(); + } + + /** + * Decodes an encoded word encoded with the 'B' encoding (described in + * RFC 2047) found in a header field body. + * + * @param encodedWord the encoded word to decode. + * @param charset the Java charset to use. + * @return the decoded string. + * @throws UnsupportedEncodingException if the given Java charset isn't + * supported. + */ + public static String decodeB(String encodedWord, String charset) + throws UnsupportedEncodingException { + + return new String(decodeBase64(encodedWord), charset); + } + + /** + * Decodes an encoded word encoded with the 'Q' encoding (described in + * RFC 2047) found in a header field body. + * + * @param encodedWord the encoded word to decode. + * @param charset the Java charset to use. + * @return the decoded string. + * @throws UnsupportedEncodingException if the given Java charset isn't + * supported. + */ + public static String decodeQ(String encodedWord, String charset) + throws UnsupportedEncodingException { + + /* + * Replace _ with =20 + */ + StringBuffer sb = new StringBuffer(); + for (int i = 0; i < encodedWord.length(); i++) { + char c = encodedWord.charAt(i); + if (c == '_') { + sb.append("=20"); + } else { + sb.append(c); + } + } + + return new String(decodeBaseQuotedPrintable(sb.toString()), charset); + } + + /** + * Decodes a string containing encoded words as defined by RFC 2047. + * Encoded words in have the form + * =?charset?enc?Encoded word?= where enc is either 'Q' or 'q' for + * quoted-printable and 'B' or 'b' for Base64. + * + * ANDROID: COPIED FROM A NEWER VERSION OF MIME4J + * + * @param body the string to decode. + * @return the decoded string. + */ + public static String decodeEncodedWords(String body) { + + // ANDROID: Most strings will not include "=?" so a quick test can prevent unneeded + // object creation. This could also be handled via lazy creation of the StringBuilder. + if (body.indexOf("=?") == -1) { + return body; + } + + int previousEnd = 0; + boolean previousWasEncoded = false; + + StringBuilder sb = new StringBuilder(); + + while (true) { + int begin = body.indexOf("=?", previousEnd); + + // ANDROID: The mime4j original version has an error here. It gets confused if + // the encoded string begins with an '=' (just after "?Q?"). This patch seeks forward + // to find the two '?' in the "header", before looking for the final "?=". + if (begin == -1) { + break; + } + int qm1 = body.indexOf('?', begin + 2); + if (qm1 == -1) { + break; + } + int qm2 = body.indexOf('?', qm1 + 1); + if (qm2 == -1) { + break; + } + int end = body.indexOf("?=", qm2 + 1); + if (end == -1) { + break; + } + end += 2; + + String sep = body.substring(previousEnd, begin); + + String decoded = decodeEncodedWord(body, begin, end); + if (decoded == null) { + sb.append(sep); + sb.append(body.substring(begin, end)); + } else { + if (!previousWasEncoded || !CharsetUtil.isWhitespace(sep)) { + sb.append(sep); + } + sb.append(decoded); + } + + previousEnd = end; + previousWasEncoded = decoded != null; + } + + if (previousEnd == 0) + return body; + + sb.append(body.substring(previousEnd)); + return sb.toString(); + } + + // return null on error. Begin is index of '=?' in body. + public static String decodeEncodedWord(String body, int begin, int end) { + // Skip the '?=' chars in body and scan forward from there for next '?' + int qm1 = body.indexOf('?', begin + 2); + if (qm1 == -1 || qm1 == end - 2) + return null; + + int qm2 = body.indexOf('?', qm1 + 1); + if (qm2 == -1 || qm2 == end - 2) + return null; + + String mimeCharset = body.substring(begin + 2, qm1); + String encoding = body.substring(qm1 + 1, qm2); + String encodedText = body.substring(qm2 + 1, end - 2); + + String charset = CharsetUtil.toJavaCharset(mimeCharset); + if (charset == null) { + if (log.isWarnEnabled()) { + log.warn("MIME charset '" + mimeCharset + "' in encoded word '" + + body.substring(begin, end) + "' doesn't have a " + + "corresponding Java charset"); + } + return null; + } else if (!CharsetUtil.isDecodingSupported(charset)) { + if (log.isWarnEnabled()) { + log.warn("Current JDK doesn't support decoding of charset '" + + charset + "' (MIME charset '" + mimeCharset + + "' in encoded word '" + body.substring(begin, end) + + "')"); + } + return null; + } + + if (encodedText.length() == 0) { + if (log.isWarnEnabled()) { + log.warn("Missing encoded text in encoded word: '" + + body.substring(begin, end) + "'"); + } + return null; + } + + try { + if (encoding.equalsIgnoreCase("Q")) { + return DecoderUtil.decodeQ(encodedText, charset); + } else if (encoding.equalsIgnoreCase("B")) { + return DecoderUtil.decodeB(encodedText, charset); + } else { + if (log.isWarnEnabled()) { + log.warn("Warning: Unknown encoding in encoded word '" + + body.substring(begin, end) + "'"); + } + return null; + } + } catch (UnsupportedEncodingException e) { + // should not happen because of isDecodingSupported check above + if (log.isWarnEnabled()) { + log.warn("Unsupported encoding in encoded word '" + + body.substring(begin, end) + "'", e); + } + return null; + } catch (RuntimeException e) { + if (log.isWarnEnabled()) { + log.warn("Could not decode encoded word '" + + body.substring(begin, end) + "'", e); + } + return null; + } + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java new file mode 100644 index 000000000..e43f398f9 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java @@ -0,0 +1,229 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.decoder; + +import java.io.IOException; +import java.io.InputStream; + +//BEGIN android-changed: Stubbing out logging +import org.apache.james.mime4j.Log; +import org.apache.james.mime4j.LogFactory; +//END android-changed + +/** + * Performs Quoted-Printable decoding on an underlying stream. + * + * + * + * @version $Id: QuotedPrintableInputStream.java,v 1.3 2004/11/29 13:15:47 ntherning Exp $ + */ +public class QuotedPrintableInputStream extends InputStream { + private static Log log = LogFactory.getLog(QuotedPrintableInputStream.class); + + private InputStream stream; + ByteQueue byteq = new ByteQueue(); + ByteQueue pushbackq = new ByteQueue(); + private byte state = 0; + + public QuotedPrintableInputStream(InputStream stream) { + this.stream = stream; + } + + /** + * Closes the underlying stream. + * + * @throws IOException on I/O errors. + */ + public void close() throws IOException { + stream.close(); + } + + public int read() throws IOException { + fillBuffer(); + if (byteq.count() == 0) + return -1; + else { + byte val = byteq.dequeue(); + if (val >= 0) + return val; + else + return val & 0xFF; + } + } + + /** + * Pulls bytes out of the underlying stream and places them in the + * pushback queue. This is necessary (vs. reading from the + * underlying stream directly) to detect and filter out "transport + * padding" whitespace, i.e., all whitespace that appears immediately + * before a CRLF. + * + * @throws IOException Underlying stream threw IOException. + */ + private void populatePushbackQueue() throws IOException { + //Debug.verify(pushbackq.count() == 0, "PopulatePushbackQueue called when pushback queue was not empty!"); + + if (pushbackq.count() != 0) + return; + + while (true) { + int i = stream.read(); + switch (i) { + case -1: + // stream is done + pushbackq.clear(); // discard any whitespace preceding EOF + return; + case ' ': + case '\t': + pushbackq.enqueue((byte)i); + break; + case '\r': + case '\n': + pushbackq.clear(); // discard any whitespace preceding EOL + pushbackq.enqueue((byte)i); + return; + default: + pushbackq.enqueue((byte)i); + return; + } + } + } + + /** + * Causes the pushback queue to get populated if it is empty, then + * consumes and decodes bytes out of it until one or more bytes are + * in the byte queue. This decoding step performs the actual QP + * decoding. + * + * @throws IOException Underlying stream threw IOException. + */ + private void fillBuffer() throws IOException { + byte msdChar = 0; // first digit of escaped num + while (byteq.count() == 0) { + if (pushbackq.count() == 0) { + populatePushbackQueue(); + if (pushbackq.count() == 0) + return; + } + + byte b = (byte)pushbackq.dequeue(); + + switch (state) { + case 0: // start state, no bytes pending + if (b != '=') { + byteq.enqueue(b); + break; // state remains 0 + } else { + state = 1; + break; + } + case 1: // encountered "=" so far + if (b == '\r') { + state = 2; + break; + } else if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) { + state = 3; + msdChar = b; // save until next digit encountered + break; + } else if (b == '=') { + /* + * Special case when == is encountered. + * Emit one = and stay in this state. + */ + if (log.isWarnEnabled()) { + log.warn("Malformed MIME; got =="); + } + byteq.enqueue((byte)'='); + break; + } else { + if (log.isWarnEnabled()) { + log.warn("Malformed MIME; expected \\r or " + + "[0-9A-Z], got " + b); + } + state = 0; + byteq.enqueue((byte)'='); + byteq.enqueue(b); + break; + } + case 2: // encountered "=\r" so far + if (b == '\n') { + state = 0; + break; + } else { + if (log.isWarnEnabled()) { + log.warn("Malformed MIME; expected " + + (int)'\n' + ", got " + b); + } + state = 0; + byteq.enqueue((byte)'='); + byteq.enqueue((byte)'\r'); + byteq.enqueue(b); + break; + } + case 3: // encountered =<digit> so far; expecting another <digit> to complete the octet + if ((b >= '0' && b <= '9') || (b >= 'A' && b <= 'F') || (b >= 'a' && b <= 'f')) { + byte msd = asciiCharToNumericValue(msdChar); + byte low = asciiCharToNumericValue(b); + state = 0; + byteq.enqueue((byte)((msd << 4) | low)); + break; + } else { + if (log.isWarnEnabled()) { + log.warn("Malformed MIME; expected " + + "[0-9A-Z], got " + b); + } + state = 0; + byteq.enqueue((byte)'='); + byteq.enqueue(msdChar); + byteq.enqueue(b); + break; + } + default: // should never happen + log.error("Illegal state: " + state); + state = 0; + byteq.enqueue(b); + break; + } + } + } + + /** + * Converts '0' => 0, 'A' => 10, etc. + * @param c ASCII character value. + * @return Numeric value of hexadecimal character. + */ + private byte asciiCharToNumericValue(byte c) { + if (c >= '0' && c <= '9') { + return (byte)(c - '0'); + } else if (c >= 'A' && c <= 'Z') { + return (byte)(0xA + (c - 'A')); + } else if (c >= 'a' && c <= 'z') { + return (byte)(0xA + (c - 'a')); + } else { + /* + * This should never happen since all calls to this method + * are preceded by a check that c is in [0-9A-Za-z] + */ + throw new IllegalArgumentException((char) c + + " is not a hexadecimal digit"); + } + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java new file mode 100644 index 000000000..f01194fd1 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java @@ -0,0 +1,272 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.decoder; + +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * UnboundedFifoByteBuffer is a very efficient buffer implementation. + * According to performance testing, it exhibits a constant access time, but it + * also outperforms ArrayList when used for the same purpose. + * <p> + * The removal order of an <code>UnboundedFifoByteBuffer</code> is based on the insertion + * order; elements are removed in the same order in which they were added. + * The iteration order is the same as the removal order. + * <p> + * The {@link #remove()} and {@link #get()} operations perform in constant time. + * The {@link #add(Object)} operation performs in amortized constant time. All + * other operations perform in linear time or worse. + * <p> + * Note that this implementation is not synchronized. The following can be + * used to provide synchronized access to your <code>UnboundedFifoByteBuffer</code>: + * <pre> + * Buffer fifo = BufferUtils.synchronizedBuffer(new UnboundedFifoByteBuffer()); + * </pre> + * <p> + * This buffer prevents null objects from being added. + * + * @since Commons Collections 3.0 (previously in main package v2.1) + * @version $Revision: 1.1 $ $Date: 2004/08/24 06:52:02 $ + * + * + * + * + * + * + */ +class UnboundedFifoByteBuffer { + + protected byte[] buffer; + protected int head; + protected int tail; + + /** + * Constructs an UnboundedFifoByteBuffer with the default number of elements. + * It is exactly the same as performing the following: + * + * <pre> + * new UnboundedFifoByteBuffer(32); + * </pre> + */ + public UnboundedFifoByteBuffer() { + this(32); + } + + /** + * Constructs an UnboundedFifoByteBuffer with the specified number of elements. + * The integer must be a positive integer. + * + * @param initialSize the initial size of the buffer + * @throws IllegalArgumentException if the size is less than 1 + */ + public UnboundedFifoByteBuffer(int initialSize) { + if (initialSize <= 0) { + throw new IllegalArgumentException("The size must be greater than 0"); + } + buffer = new byte[initialSize + 1]; + head = 0; + tail = 0; + } + + /** + * Returns the number of elements stored in the buffer. + * + * @return this buffer's size + */ + public int size() { + int size = 0; + + if (tail < head) { + size = buffer.length - head + tail; + } else { + size = tail - head; + } + + return size; + } + + /** + * Returns true if this buffer is empty; false otherwise. + * + * @return true if this buffer is empty + */ + public boolean isEmpty() { + return (size() == 0); + } + + /** + * Adds the given element to this buffer. + * + * @param b the byte to add + * @return true, always + */ + public boolean add(final byte b) { + + if (size() + 1 >= buffer.length) { + byte[] tmp = new byte[((buffer.length - 1) * 2) + 1]; + + int j = 0; + for (int i = head; i != tail;) { + tmp[j] = buffer[i]; + buffer[i] = 0; + + j++; + i++; + if (i == buffer.length) { + i = 0; + } + } + + buffer = tmp; + head = 0; + tail = j; + } + + buffer[tail] = b; + tail++; + if (tail >= buffer.length) { + tail = 0; + } + return true; + } + + /** + * Returns the next object in the buffer. + * + * @return the next object in the buffer + * @throws BufferUnderflowException if this buffer is empty + */ + public byte get() { + if (isEmpty()) { + throw new IllegalStateException("The buffer is already empty"); + } + + return buffer[head]; + } + + /** + * Removes the next object from the buffer + * + * @return the removed object + * @throws BufferUnderflowException if this buffer is empty + */ + public byte remove() { + if (isEmpty()) { + throw new IllegalStateException("The buffer is already empty"); + } + + byte element = buffer[head]; + + head++; + if (head >= buffer.length) { + head = 0; + } + + return element; + } + + /** + * Increments the internal index. + * + * @param index the index to increment + * @return the updated index + */ + private int increment(int index) { + index++; + if (index >= buffer.length) { + index = 0; + } + return index; + } + + /** + * Decrements the internal index. + * + * @param index the index to decrement + * @return the updated index + */ + private int decrement(int index) { + index--; + if (index < 0) { + index = buffer.length - 1; + } + return index; + } + + /** + * Returns an iterator over this buffer's elements. + * + * @return an iterator over this buffer's elements + */ + public Iterator iterator() { + return new Iterator() { + + private int index = head; + private int lastReturnedIndex = -1; + + public boolean hasNext() { + return index != tail; + + } + + public Object next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + lastReturnedIndex = index; + index = increment(index); + return new Byte(buffer[lastReturnedIndex]); + } + + public void remove() { + if (lastReturnedIndex == -1) { + throw new IllegalStateException(); + } + + // First element can be removed quickly + if (lastReturnedIndex == head) { + UnboundedFifoByteBuffer.this.remove(); + lastReturnedIndex = -1; + return; + } + + // Other elements require us to shift the subsequent elements + int i = lastReturnedIndex + 1; + while (i != tail) { + if (i >= buffer.length) { + buffer[i - 1] = buffer[0]; + i = 0; + } else { + buffer[i - 1] = buffer[i]; + i++; + } + } + + lastReturnedIndex = -1; + tail = decrement(tail); + buffer[tail] = 0; + index = decrement(index); + } + + }; + } + +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java new file mode 100644 index 000000000..df9f39835 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java @@ -0,0 +1,65 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field; + +//BEGIN android-changed: Stubbing out logging +import org.apache.james.mime4j.Log; +import org.apache.james.mime4j.LogFactory; +//END android-changed +import org.apache.james.mime4j.field.address.AddressList; +import org.apache.james.mime4j.field.address.parser.ParseException; + +public class AddressListField extends Field { + private AddressList addressList; + private ParseException parseException; + + protected AddressListField(String name, String body, String raw, AddressList addressList, ParseException parseException) { + super(name, body, raw); + this.addressList = addressList; + this.parseException = parseException; + } + + public AddressList getAddressList() { + return addressList; + } + + public ParseException getParseException() { + return parseException; + } + + public static class Parser implements FieldParser { + private static Log log = LogFactory.getLog(Parser.class); + + public Field parse(final String name, final String body, final String raw) { + AddressList addressList = null; + ParseException parseException = null; + try { + addressList = AddressList.parse(body); + } + catch (ParseException e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = e; + } + return new AddressListField(name, body, raw, addressList, parseException); + } + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java new file mode 100644 index 000000000..73d8d2339 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java @@ -0,0 +1,88 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field; + + + +/** + * Represents a <code>Content-Transfer-Encoding</code> field. + * + * + * @version $Id: ContentTransferEncodingField.java,v 1.2 2004/10/02 12:41:11 ntherning Exp $ + */ +public class ContentTransferEncodingField extends Field { + /** + * The <code>7bit</code> encoding. + */ + public static final String ENC_7BIT = "7bit"; + /** + * The <code>8bit</code> encoding. + */ + public static final String ENC_8BIT = "8bit"; + /** + * The <code>binary</code> encoding. + */ + public static final String ENC_BINARY = "binary"; + /** + * The <code>quoted-printable</code> encoding. + */ + public static final String ENC_QUOTED_PRINTABLE = "quoted-printable"; + /** + * The <code>base64</code> encoding. + */ + public static final String ENC_BASE64 = "base64"; + + private String encoding; + + protected ContentTransferEncodingField(String name, String body, String raw, String encoding) { + super(name, body, raw); + this.encoding = encoding; + } + + /** + * Gets the encoding defined in this field. + * + * @return the encoding or an empty string if not set. + */ + public String getEncoding() { + return encoding; + } + + /** + * Gets the encoding of the given field if. Returns the default + * <code>7bit</code> if not set or if + * <code>f</code> is <code>null</code>. + * + * @return the encoding. + */ + public static String getEncoding(ContentTransferEncodingField f) { + if (f != null && f.getEncoding().length() != 0) { + return f.getEncoding(); + } + return ENC_7BIT; + } + + public static class Parser implements FieldParser { + public Field parse(final String name, final String body, final String raw) { + final String encoding = body.trim().toLowerCase(); + return new ContentTransferEncodingField(name, body, raw, encoding); + } + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java new file mode 100644 index 000000000..ad9f7f9ac --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java @@ -0,0 +1,259 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field; + +import java.io.StringReader; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +//BEGIN android-changed: Stubbing out logging +import org.apache.james.mime4j.Log; +import org.apache.james.mime4j.LogFactory; +//END android-changed +import org.apache.james.mime4j.field.contenttype.parser.ContentTypeParser; +import org.apache.james.mime4j.field.contenttype.parser.ParseException; +import org.apache.james.mime4j.field.contenttype.parser.TokenMgrError; + +/** + * Represents a <code>Content-Type</code> field. + * + * <p>TODO: Remove dependency on Java 1.4 regexps</p> + * + * + * @version $Id: ContentTypeField.java,v 1.6 2005/01/27 14:16:31 ntherning Exp $ + */ +public class ContentTypeField extends Field { + + /** + * The prefix of all <code>multipart</code> MIME types. + */ + public static final String TYPE_MULTIPART_PREFIX = "multipart/"; + /** + * The <code>multipart/digest</code> MIME type. + */ + public static final String TYPE_MULTIPART_DIGEST = "multipart/digest"; + /** + * The <code>text/plain</code> MIME type. + */ + public static final String TYPE_TEXT_PLAIN = "text/plain"; + /** + * The <code>message/rfc822</code> MIME type. + */ + public static final String TYPE_MESSAGE_RFC822 = "message/rfc822"; + /** + * The name of the <code>boundary</code> parameter. + */ + public static final String PARAM_BOUNDARY = "boundary"; + /** + * The name of the <code>charset</code> parameter. + */ + public static final String PARAM_CHARSET = "charset"; + + private String mimeType = ""; + private Map<String, String> parameters = null; + private ParseException parseException; + + protected ContentTypeField(String name, String body, String raw, String mimeType, Map<String, String> parameters, ParseException parseException) { + super(name, body, raw); + this.mimeType = mimeType; + this.parameters = parameters; + this.parseException = parseException; + } + + /** + * Gets the exception that was raised during parsing of + * the field value, if any; otherwise, null. + */ + public ParseException getParseException() { + return parseException; + } + + /** + * Gets the MIME type defined in this Content-Type field. + * + * @return the MIME type or an empty string if not set. + */ + public String getMimeType() { + return mimeType; + } + + /** + * Gets the MIME type defined in the child's + * Content-Type field or derives a MIME type from the parent + * if child is <code>null</code> or hasn't got a MIME type value set. + * If child's MIME type is multipart but no boundary + * has been set the MIME type of child will be derived from + * the parent. + * + * @param child the child. + * @param parent the parent. + * @return the MIME type. + */ + public static String getMimeType(ContentTypeField child, + ContentTypeField parent) { + + if (child == null || child.getMimeType().length() == 0 + || child.isMultipart() && child.getBoundary() == null) { + + if (parent != null && parent.isMimeType(TYPE_MULTIPART_DIGEST)) { + return TYPE_MESSAGE_RFC822; + } else { + return TYPE_TEXT_PLAIN; + } + } + + return child.getMimeType(); + } + + /** + * Gets the value of a parameter. Parameter names are case-insensitive. + * + * @param name the name of the parameter to get. + * @return the parameter value or <code>null</code> if not set. + */ + public String getParameter(String name) { + return parameters != null + ? parameters.get(name.toLowerCase()) + : null; + } + + /** + * Gets all parameters. + * + * @return the parameters. + */ + public Map<String, String> getParameters() { + if (parameters != null) { + return Collections.unmodifiableMap(parameters); + } + return Collections.emptyMap(); + } + + /** + * Gets the value of the <code>boundary</code> parameter if set. + * + * @return the <code>boundary</code> parameter value or <code>null</code> + * if not set. + */ + public String getBoundary() { + return getParameter(PARAM_BOUNDARY); + } + + /** + * Gets the value of the <code>charset</code> parameter if set. + * + * @return the <code>charset</code> parameter value or <code>null</code> + * if not set. + */ + public String getCharset() { + return getParameter(PARAM_CHARSET); + } + + /** + * Gets the value of the <code>charset</code> parameter if set for the + * given field. Returns the default <code>us-ascii</code> if not set or if + * <code>f</code> is <code>null</code>. + * + * @return the <code>charset</code> parameter value. + */ + public static String getCharset(ContentTypeField f) { + if (f != null) { + if (f.getCharset() != null && f.getCharset().length() > 0) { + return f.getCharset(); + } + } + return "us-ascii"; + } + + /** + * Determines if the MIME type of this field matches the given one. + * + * @param mimeType the MIME type to match against. + * @return <code>true</code> if the MIME type of this field matches, + * <code>false</code> otherwise. + */ + public boolean isMimeType(String mimeType) { + return this.mimeType.equalsIgnoreCase(mimeType); + } + + /** + * Determines if the MIME type of this field is <code>multipart/*</code>. + * + * @return <code>true</code> if this field is has a <code>multipart/*</code> + * MIME type, <code>false</code> otherwise. + */ + public boolean isMultipart() { + return mimeType.startsWith(TYPE_MULTIPART_PREFIX); + } + + public static class Parser implements FieldParser { + private static Log log = LogFactory.getLog(Parser.class); + + public Field parse(final String name, final String body, final String raw) { + ParseException parseException = null; + String mimeType = ""; + Map<String, String> parameters = null; + + ContentTypeParser parser = new ContentTypeParser(new StringReader(body)); + try { + parser.parseAll(); + } + catch (ParseException e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = e; + } + catch (TokenMgrError e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = new ParseException(e.getMessage()); + } + + try { + final String type = parser.getType(); + final String subType = parser.getSubType(); + + if (type != null && subType != null) { + mimeType = (type + "/" + parser.getSubType()).toLowerCase(); + + ArrayList<String> paramNames = parser.getParamNames(); + ArrayList<String> paramValues = parser.getParamValues(); + + if (paramNames != null && paramValues != null) { + for (int i = 0; i < paramNames.size() && i < paramValues.size(); i++) { + if (parameters == null) + parameters = new HashMap<String, String>((int)(paramNames.size() * 1.3 + 1)); + String paramName = paramNames.get(i).toLowerCase(); + String paramValue = paramValues.get(i); + parameters.put(paramName, paramValue); + } + } + } + } + catch (NullPointerException npe) { + } + return new ContentTypeField(name, body, raw, mimeType, parameters, parseException); + } + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java new file mode 100644 index 000000000..1e6c8e250 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java @@ -0,0 +1,73 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field; + +//BEGIN android-changed: Stubbing out logging + +import com.android.voicemailomtp.mail.utils.LogUtils; + +import org.apache.james.mime4j.Log; +import org.apache.james.mime4j.LogFactory; +//END +import org.apache.james.mime4j.field.datetime.DateTime; +import org.apache.james.mime4j.field.datetime.parser.ParseException; + +import java.util.Date; + +public class DateTimeField extends Field { + private Date date; + private ParseException parseException; + + protected DateTimeField(String name, String body, String raw, Date date, ParseException parseException) { + super(name, body, raw); + this.date = date; + this.parseException = parseException; + } + + public Date getDate() { + return date; + } + + public ParseException getParseException() { + return parseException; + } + + public static class Parser implements FieldParser { + private static Log log = LogFactory.getLog(Parser.class); + + public Field parse(final String name, String body, final String raw) { + Date date = null; + ParseException parseException = null; + //BEGIN android-changed + body = LogUtils.cleanUpMimeDate(body); + //END android-changed + try { + date = DateTime.parse(body).getDate(); + } + catch (ParseException e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = e; + } + return new DateTimeField(name, body, raw, date, parseException); + } + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java new file mode 100644 index 000000000..3695afe3e --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java @@ -0,0 +1,45 @@ +/* + * Copyright 2006 the mime4j 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 org.apache.james.mime4j.field; + +public class DefaultFieldParser extends DelegatingFieldParser { + + public DefaultFieldParser() { + setFieldParser(Field.CONTENT_TRANSFER_ENCODING, new ContentTransferEncodingField.Parser()); + setFieldParser(Field.CONTENT_TYPE, new ContentTypeField.Parser()); + + final DateTimeField.Parser dateTimeParser = new DateTimeField.Parser(); + setFieldParser(Field.DATE, dateTimeParser); + setFieldParser(Field.RESENT_DATE, dateTimeParser); + + final MailboxListField.Parser mailboxListParser = new MailboxListField.Parser(); + setFieldParser(Field.FROM, mailboxListParser); + setFieldParser(Field.RESENT_FROM, mailboxListParser); + + final MailboxField.Parser mailboxParser = new MailboxField.Parser(); + setFieldParser(Field.SENDER, mailboxParser); + setFieldParser(Field.RESENT_SENDER, mailboxParser); + + final AddressListField.Parser addressListParser = new AddressListField.Parser(); + setFieldParser(Field.TO, addressListParser); + setFieldParser(Field.RESENT_TO, addressListParser); + setFieldParser(Field.CC, addressListParser); + setFieldParser(Field.RESENT_CC, addressListParser); + setFieldParser(Field.BCC, addressListParser); + setFieldParser(Field.RESENT_BCC, addressListParser); + setFieldParser(Field.REPLY_TO, addressListParser); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java new file mode 100644 index 000000000..32b69ec13 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java @@ -0,0 +1,47 @@ +/* + * Copyright 2006 the mime4j 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 org.apache.james.mime4j.field; + +import java.util.HashMap; +import java.util.Map; + +public class DelegatingFieldParser implements FieldParser { + + private Map<String, FieldParser> parsers = new HashMap<String, FieldParser>(); + private FieldParser defaultParser = new UnstructuredField.Parser(); + + /** + * Sets the parser used for the field named <code>name</code>. + * @param name the name of the field + * @param parser the parser for fields named <code>name</code> + */ + public void setFieldParser(final String name, final FieldParser parser) { + parsers.put(name.toLowerCase(), parser); + } + + public FieldParser getParser(final String name) { + final FieldParser field = parsers.get(name.toLowerCase()); + if(field==null) { + return defaultParser; + } + return field; + } + + public Field parse(final String name, final String body, final String raw) { + final FieldParser parser = getParser(name); + return parser.parse(name, body, raw); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java new file mode 100644 index 000000000..4dea5c5cf --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java @@ -0,0 +1,192 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * The base class of all field classes. + * + * + * @version $Id: Field.java,v 1.6 2004/10/25 07:26:46 ntherning Exp $ + */ +public abstract class Field { + public static final String SENDER = "Sender"; + public static final String FROM = "From"; + public static final String TO = "To"; + public static final String CC = "Cc"; + public static final String BCC = "Bcc"; + public static final String REPLY_TO = "Reply-To"; + public static final String RESENT_SENDER = "Resent-Sender"; + public static final String RESENT_FROM = "Resent-From"; + public static final String RESENT_TO = "Resent-To"; + public static final String RESENT_CC = "Resent-Cc"; + public static final String RESENT_BCC = "Resent-Bcc"; + + public static final String DATE = "Date"; + public static final String RESENT_DATE = "Resent-Date"; + + public static final String SUBJECT = "Subject"; + public static final String CONTENT_TYPE = "Content-Type"; + public static final String CONTENT_TRANSFER_ENCODING = + "Content-Transfer-Encoding"; + + private static final String FIELD_NAME_PATTERN = + "^([\\x21-\\x39\\x3b-\\x7e]+)[ \t]*:"; + private static final Pattern fieldNamePattern = + Pattern.compile(FIELD_NAME_PATTERN); + + private static final DefaultFieldParser parser = new DefaultFieldParser(); + + private final String name; + private final String body; + private final String raw; + + protected Field(final String name, final String body, final String raw) { + this.name = name; + this.body = body; + this.raw = raw; + } + + /** + * Parses the given string and returns an instance of the + * <code>Field</code> class. The type of the class returned depends on + * the field name: + * <table> + * <tr> + * <td><em>Field name</em></td><td><em>Class returned</em></td> + * <td>Content-Type</td><td>org.apache.james.mime4j.field.ContentTypeField</td> + * <td>other</td><td>org.apache.james.mime4j.field.UnstructuredField</td> + * </tr> + * </table> + * + * @param s the string to parse. + * @return a <code>Field</code> instance. + * @throws IllegalArgumentException on parse errors. + */ + public static Field parse(final String raw) { + + /* + * Unfold the field. + */ + final String unfolded = raw.replaceAll("\r|\n", ""); + + /* + * Split into name and value. + */ + final Matcher fieldMatcher = fieldNamePattern.matcher(unfolded); + if (!fieldMatcher.find()) { + throw new IllegalArgumentException("Invalid field in string"); + } + final String name = fieldMatcher.group(1); + + String body = unfolded.substring(fieldMatcher.end()); + if (body.length() > 0 && body.charAt(0) == ' ') { + body = body.substring(1); + } + + return parser.parse(name, body, raw); + } + + /** + * Gets the default parser used to parse fields. + * @return the default field parser + */ + public static DefaultFieldParser getParser() { + return parser; + } + + /** + * Gets the name of the field (<code>Subject</code>, + * <code>From</code>, etc). + * + * @return the field name. + */ + public String getName() { + return name; + } + + /** + * Gets the original raw field string. + * + * @return the original raw field string. + */ + public String getRaw() { + return raw; + } + + /** + * Gets the unfolded, unparsed and possibly encoded (see RFC 2047) field + * body string. + * + * @return the unfolded unparsed field body string. + */ + public String getBody() { + return body; + } + + /** + * Determines if this is a <code>Content-Type</code> field. + * + * @return <code>true</code> if this is a <code>Content-Type</code> field, + * <code>false</code> otherwise. + */ + public boolean isContentType() { + return CONTENT_TYPE.equalsIgnoreCase(name); + } + + /** + * Determines if this is a <code>Subject</code> field. + * + * @return <code>true</code> if this is a <code>Subject</code> field, + * <code>false</code> otherwise. + */ + public boolean isSubject() { + return SUBJECT.equalsIgnoreCase(name); + } + + /** + * Determines if this is a <code>From</code> field. + * + * @return <code>true</code> if this is a <code>From</code> field, + * <code>false</code> otherwise. + */ + public boolean isFrom() { + return FROM.equalsIgnoreCase(name); + } + + /** + * Determines if this is a <code>To</code> field. + * + * @return <code>true</code> if this is a <code>To</code> field, + * <code>false</code> otherwise. + */ + public boolean isTo() { + return TO.equalsIgnoreCase(name); + } + + /** + * @see #getRaw() + */ + public String toString() { + return raw; + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java new file mode 100644 index 000000000..78aaf1334 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java @@ -0,0 +1,21 @@ +/* + * Copyright 2006 the mime4j 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 org.apache.james.mime4j.field; + +public interface FieldParser { + + Field parse(final String name, final String body, final String raw); +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java new file mode 100644 index 000000000..f15980055 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java @@ -0,0 +1,70 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field; + +//BEGIN android-changed: Stubbing out logging +import org.apache.james.mime4j.Log; +import org.apache.james.mime4j.LogFactory; +//END android-changed +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.parser.ParseException; + +public class MailboxField extends Field { + private final Mailbox mailbox; + private final ParseException parseException; + + protected MailboxField(final String name, final String body, final String raw, final Mailbox mailbox, final ParseException parseException) { + super(name, body, raw); + this.mailbox = mailbox; + this.parseException = parseException; + } + + public Mailbox getMailbox() { + return mailbox; + } + + public ParseException getParseException() { + return parseException; + } + + public static class Parser implements FieldParser { + private static Log log = LogFactory.getLog(Parser.class); + + public Field parse(final String name, final String body, final String raw) { + Mailbox mailbox = null; + ParseException parseException = null; + try { + MailboxList mailboxList = AddressList.parse(body).flatten(); + if (mailboxList.size() > 0) { + mailbox = mailboxList.get(0); + } + } + catch (ParseException e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = e; + } + return new MailboxField(name, body, raw, mailbox, parseException); + } + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java new file mode 100644 index 000000000..23378d4fa --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java @@ -0,0 +1,67 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field; + +//BEGIN android-changed: Stubbing out logging +import org.apache.james.mime4j.Log; +import org.apache.james.mime4j.LogFactory; +//END android-changed +import org.apache.james.mime4j.field.address.AddressList; +import org.apache.james.mime4j.field.address.MailboxList; +import org.apache.james.mime4j.field.address.parser.ParseException; + +public class MailboxListField extends Field { + + private MailboxList mailboxList; + private ParseException parseException; + + protected MailboxListField(final String name, final String body, final String raw, final MailboxList mailboxList, final ParseException parseException) { + super(name, body, raw); + this.mailboxList = mailboxList; + this.parseException = parseException; + } + + public MailboxList getMailboxList() { + return mailboxList; + } + + public ParseException getParseException() { + return parseException; + } + + public static class Parser implements FieldParser { + private static Log log = LogFactory.getLog(Parser.class); + + public Field parse(final String name, final String body, final String raw) { + MailboxList mailboxList = null; + ParseException parseException = null; + try { + mailboxList = AddressList.parse(body).flatten(); + } + catch (ParseException e) { + if (log.isDebugEnabled()) { + log.debug("Parsing value '" + body + "': "+ e.getMessage()); + } + parseException = e; + } + return new MailboxListField(name, body, raw, mailboxList, parseException); + } + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java new file mode 100644 index 000000000..6084e4435 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java @@ -0,0 +1,49 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field; + +import org.apache.james.mime4j.decoder.DecoderUtil; + + +/** + * Simple unstructured field such as <code>Subject</code>. + * + * + * @version $Id: UnstructuredField.java,v 1.3 2004/10/25 07:26:46 ntherning Exp $ + */ +public class UnstructuredField extends Field { + private String value; + + protected UnstructuredField(String name, String body, String raw, String value) { + super(name, body, raw); + this.value = value; + } + + public String getValue() { + return value; + } + + public static class Parser implements FieldParser { + public Field parse(final String name, final String body, final String raw) { + final String value = DecoderUtil.decodeEncodedWords(body); + return new UnstructuredField(name, body, raw, value); + } + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java new file mode 100644 index 000000000..3e24e91aa --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java @@ -0,0 +1,52 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field.address; + +import java.util.ArrayList; + +/** + * The abstract base for classes that represent RFC2822 addresses. + * This includes groups and mailboxes. + * + * Currently, no public methods are introduced on this class. + * + * + */ +public abstract class Address { + + /** + * Adds any mailboxes represented by this address + * into the given ArrayList. Note that this method + * has default (package) access, so a doAddMailboxesTo + * method is needed to allow the behavior to be + * overridden by subclasses. + */ + final void addMailboxesTo(ArrayList<Address> results) { + doAddMailboxesTo(results); + } + + /** + * Adds any mailboxes represented by this address + * into the given ArrayList. Must be overridden by + * concrete subclasses. + */ + protected abstract void doAddMailboxesTo(ArrayList<Address> results); + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java new file mode 100644 index 000000000..1829e79aa --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java @@ -0,0 +1,138 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field.address; + +import org.apache.james.mime4j.field.address.parser.AddressListParser; +import org.apache.james.mime4j.field.address.parser.ParseException; + +import java.io.StringReader; +import java.util.ArrayList; + +/** + * An immutable, random-access list of Address objects. + * + * + */ +public class AddressList { + + private ArrayList<Address> addresses; + + /** + * @param addresses An ArrayList that contains only Address objects. + * @param dontCopy true iff it is not possible for the addresses ArrayList to be modified by someone else. + */ + public AddressList(ArrayList<Address> addresses, boolean dontCopy) { + if (addresses != null) + this.addresses = (dontCopy ? addresses : new ArrayList<Address>(addresses)); + else + this.addresses = new ArrayList<Address>(0); + } + + /** + * The number of elements in this list. + */ + public int size() { + return addresses.size(); + } + + /** + * Gets an address. + */ + public Address get(int index) { + if (0 > index || size() <= index) + throw new IndexOutOfBoundsException(); + return addresses.get(index); + } + + /** + * Returns a flat list of all mailboxes represented + * in this address list. Use this if you don't care + * about grouping. + */ + public MailboxList flatten() { + // in the common case, all addresses are mailboxes + boolean groupDetected = false; + for (int i = 0; i < size(); i++) { + if (!(get(i) instanceof Mailbox)) { + groupDetected = true; + break; + } + } + + if (!groupDetected) + return new MailboxList(addresses, true); + + ArrayList<Address> results = new ArrayList<Address>(); + for (int i = 0; i < size(); i++) { + Address addr = get(i); + addr.addMailboxesTo(results); + } + + // copy-on-construct this time, because subclasses + // could have held onto a reference to the results + return new MailboxList(results, false); + } + + /** + * Dumps a representation of this address list to + * stdout, for debugging purposes. + */ + public void print() { + for (int i = 0; i < size(); i++) { + Address addr = get(i); + System.out.println(addr.toString()); + } + } + + /** + * Parse the address list string, such as the value + * of a From, To, Cc, Bcc, Sender, or Reply-To + * header. + * + * The string MUST be unfolded already. + */ + public static AddressList parse(String rawAddressList) throws ParseException { + AddressListParser parser = new AddressListParser(new StringReader(rawAddressList)); + return Builder.getInstance().buildAddressList(parser.parse()); + } + + /** + * Test console. + */ + public static void main(String[] args) throws Exception { + java.io.BufferedReader reader = new java.io.BufferedReader(new java.io.InputStreamReader(System.in)); + while (true) { + try { + System.out.print("> "); + String line = reader.readLine(); + if (line.length() == 0 || line.toLowerCase().equals("exit") || line.toLowerCase().equals("quit")) { + System.out.println("Goodbye."); + return; + } + AddressList list = parse(line); + list.print(); + } + catch(Exception e) { + e.printStackTrace(); + Thread.sleep(300); + } + } + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java new file mode 100644 index 000000000..3bcd15b6f --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java @@ -0,0 +1,243 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field.address; + +import java.util.ArrayList; +import java.util.Iterator; + +import org.apache.james.mime4j.decoder.DecoderUtil; +import org.apache.james.mime4j.field.address.parser.ASTaddr_spec; +import org.apache.james.mime4j.field.address.parser.ASTaddress; +import org.apache.james.mime4j.field.address.parser.ASTaddress_list; +import org.apache.james.mime4j.field.address.parser.ASTangle_addr; +import org.apache.james.mime4j.field.address.parser.ASTdomain; +import org.apache.james.mime4j.field.address.parser.ASTgroup_body; +import org.apache.james.mime4j.field.address.parser.ASTlocal_part; +import org.apache.james.mime4j.field.address.parser.ASTmailbox; +import org.apache.james.mime4j.field.address.parser.ASTname_addr; +import org.apache.james.mime4j.field.address.parser.ASTphrase; +import org.apache.james.mime4j.field.address.parser.ASTroute; +import org.apache.james.mime4j.field.address.parser.Node; +import org.apache.james.mime4j.field.address.parser.SimpleNode; +import org.apache.james.mime4j.field.address.parser.Token; + +/** + * Transforms the JJTree-generated abstract syntax tree + * into a graph of org.apache.james.mime4j.field.address objects. + * + * + */ +class Builder { + + private static Builder singleton = new Builder(); + + public static Builder getInstance() { + return singleton; + } + + + + public AddressList buildAddressList(ASTaddress_list node) { + ArrayList<Address> list = new ArrayList<Address>(); + for (int i = 0; i < node.jjtGetNumChildren(); i++) { + ASTaddress childNode = (ASTaddress) node.jjtGetChild(i); + Address address = buildAddress(childNode); + list.add(address); + } + return new AddressList(list, true); + } + + private Address buildAddress(ASTaddress node) { + ChildNodeIterator it = new ChildNodeIterator(node); + Node n = it.nextNode(); + if (n instanceof ASTaddr_spec) { + return buildAddrSpec((ASTaddr_spec)n); + } + else if (n instanceof ASTangle_addr) { + return buildAngleAddr((ASTangle_addr)n); + } + else if (n instanceof ASTphrase) { + String name = buildString((ASTphrase)n, false); + Node n2 = it.nextNode(); + if (n2 instanceof ASTgroup_body) { + return new Group(name, buildGroupBody((ASTgroup_body)n2)); + } + else if (n2 instanceof ASTangle_addr) { + name = DecoderUtil.decodeEncodedWords(name); + return new NamedMailbox(name, buildAngleAddr((ASTangle_addr)n2)); + } + else { + throw new IllegalStateException(); + } + } + else { + throw new IllegalStateException(); + } + } + + + + private MailboxList buildGroupBody(ASTgroup_body node) { + ArrayList<Address> results = new ArrayList<Address>(); + ChildNodeIterator it = new ChildNodeIterator(node); + while (it.hasNext()) { + Node n = it.nextNode(); + if (n instanceof ASTmailbox) + results.add(buildMailbox((ASTmailbox)n)); + else + throw new IllegalStateException(); + } + return new MailboxList(results, true); + } + + private Mailbox buildMailbox(ASTmailbox node) { + ChildNodeIterator it = new ChildNodeIterator(node); + Node n = it.nextNode(); + if (n instanceof ASTaddr_spec) { + return buildAddrSpec((ASTaddr_spec)n); + } + else if (n instanceof ASTangle_addr) { + return buildAngleAddr((ASTangle_addr)n); + } + else if (n instanceof ASTname_addr) { + return buildNameAddr((ASTname_addr)n); + } + else { + throw new IllegalStateException(); + } + } + + private NamedMailbox buildNameAddr(ASTname_addr node) { + ChildNodeIterator it = new ChildNodeIterator(node); + Node n = it.nextNode(); + String name; + if (n instanceof ASTphrase) { + name = buildString((ASTphrase)n, false); + } + else { + throw new IllegalStateException(); + } + + n = it.nextNode(); + if (n instanceof ASTangle_addr) { + name = DecoderUtil.decodeEncodedWords(name); + return new NamedMailbox(name, buildAngleAddr((ASTangle_addr) n)); + } + else { + throw new IllegalStateException(); + } + } + + private Mailbox buildAngleAddr(ASTangle_addr node) { + ChildNodeIterator it = new ChildNodeIterator(node); + DomainList route = null; + Node n = it.nextNode(); + if (n instanceof ASTroute) { + route = buildRoute((ASTroute)n); + n = it.nextNode(); + } + else if (n instanceof ASTaddr_spec) + ; // do nothing + else + throw new IllegalStateException(); + + if (n instanceof ASTaddr_spec) + return buildAddrSpec(route, (ASTaddr_spec)n); + else + throw new IllegalStateException(); + } + + private DomainList buildRoute(ASTroute node) { + ArrayList<String> results = new ArrayList<String>(node.jjtGetNumChildren()); + ChildNodeIterator it = new ChildNodeIterator(node); + while (it.hasNext()) { + Node n = it.nextNode(); + if (n instanceof ASTdomain) + results.add(buildString((ASTdomain)n, true)); + else + throw new IllegalStateException(); + } + return new DomainList(results, true); + } + + private Mailbox buildAddrSpec(ASTaddr_spec node) { + return buildAddrSpec(null, node); + } + private Mailbox buildAddrSpec(DomainList route, ASTaddr_spec node) { + ChildNodeIterator it = new ChildNodeIterator(node); + String localPart = buildString((ASTlocal_part)it.nextNode(), true); + String domain = buildString((ASTdomain)it.nextNode(), true); + return new Mailbox(route, localPart, domain); + } + + + private String buildString(SimpleNode node, boolean stripSpaces) { + Token head = node.firstToken; + Token tail = node.lastToken; + StringBuffer out = new StringBuffer(); + + while (head != tail) { + out.append(head.image); + head = head.next; + if (!stripSpaces) + addSpecials(out, head.specialToken); + } + out.append(tail.image); + + return out.toString(); + } + + private void addSpecials(StringBuffer out, Token specialToken) { + if (specialToken != null) { + addSpecials(out, specialToken.specialToken); + out.append(specialToken.image); + } + } + + private static class ChildNodeIterator implements Iterator<Node> { + + private SimpleNode simpleNode; + private int index; + private int len; + + public ChildNodeIterator(SimpleNode simpleNode) { + this.simpleNode = simpleNode; + this.len = simpleNode.jjtGetNumChildren(); + this.index = 0; + } + + public void remove() { + throw new UnsupportedOperationException(); + } + + public boolean hasNext() { + return index < len; + } + + public Node next() { + return nextNode(); + } + + public Node nextNode() { + return simpleNode.jjtGetChild(index++); + } + + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java new file mode 100644 index 000000000..49b0f3be5 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java @@ -0,0 +1,76 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field.address; + +import java.util.ArrayList; + +/** + * An immutable, random-access list of Strings (that + * are supposedly domain names or domain literals). + * + * + */ +public class DomainList { + private ArrayList<String> domains; + + /** + * @param domains An ArrayList that contains only String objects. + * @param dontCopy true iff it is not possible for the domains ArrayList to be modified by someone else. + */ + public DomainList(ArrayList<String> domains, boolean dontCopy) { + if (domains != null) + this.domains = (dontCopy ? domains : new ArrayList<String>(domains)); + else + this.domains = new ArrayList<String>(0); + } + + /** + * The number of elements in this list. + */ + public int size() { + return domains.size(); + } + + /** + * Gets the domain name or domain literal at the + * specified index. + * @throws IndexOutOfBoundsException If index is < 0 or >= size(). + */ + public String get(int index) { + if (0 > index || size() <= index) + throw new IndexOutOfBoundsException(); + return domains.get(index); + } + + /** + * Returns the list of domains formatted as a route + * string (not including the trailing ':'). + */ + public String toRouteString() { + StringBuffer out = new StringBuffer(); + for (int i = 0; i < domains.size(); i++) { + out.append("@"); + out.append(get(i)); + if (i + 1 < domains.size()) + out.append(","); + } + return out.toString(); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java new file mode 100644 index 000000000..c0ab7f724 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java @@ -0,0 +1,75 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field.address; + +import java.util.ArrayList; + +/** + * A named group of zero or more mailboxes. + * + * + */ +public class Group extends Address { + private String name; + private MailboxList mailboxList; + + /** + * @param name The group name. + * @param mailboxes The mailboxes in this group. + */ + public Group(String name, MailboxList mailboxes) { + this.name = name; + this.mailboxList = mailboxes; + } + + /** + * Returns the group name. + */ + public String getName() { + return name; + } + + /** + * Returns the mailboxes in this group. + */ + public MailboxList getMailboxes() { + return mailboxList; + } + + @Override + public String toString() { + StringBuffer buf = new StringBuffer(); + buf.append(name); + buf.append(":"); + for (int i = 0; i < mailboxList.size(); i++) { + buf.append(mailboxList.get(i).toString()); + if (i + 1 < mailboxList.size()) + buf.append(","); + } + buf.append(";"); + return buf.toString(); + } + + @Override + protected void doAddMailboxesTo(ArrayList<Address> results) { + for (int i = 0; i < mailboxList.size(); i++) + results.add(mailboxList.get(i)); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java new file mode 100644 index 000000000..25f2548d4 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java @@ -0,0 +1,121 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field.address; + +import java.util.ArrayList; + +/** + * Represents a single e-mail address. + * + * + */ +public class Mailbox extends Address { + private DomainList route; + private String localPart; + private String domain; + + /** + * Creates a mailbox without a route. Routes are obsolete. + * @param localPart The part of the e-mail address to the left of the "@". + * @param domain The part of the e-mail address to the right of the "@". + */ + public Mailbox(String localPart, String domain) { + this(null, localPart, domain); + } + + /** + * Creates a mailbox with a route. Routes are obsolete. + * @param route The zero or more domains that make up the route. Can be null. + * @param localPart The part of the e-mail address to the left of the "@". + * @param domain The part of the e-mail address to the right of the "@". + */ + public Mailbox(DomainList route, String localPart, String domain) { + this.route = route; + this.localPart = localPart; + this.domain = domain; + } + + /** + * Returns the route list. + */ + public DomainList getRoute() { + return route; + } + + /** + * Returns the left part of the e-mail address + * (before "@"). + */ + public String getLocalPart() { + return localPart; + } + + /** + * Returns the right part of the e-mail address + * (after "@"). + */ + public String getDomain() { + return domain; + } + + /** + * Formats the address as a string, not including + * the route. + * + * @see #getAddressString(boolean) + */ + public String getAddressString() { + return getAddressString(false); + } + + /** + * Note that this value may not be usable + * for transport purposes, only display purposes. + * + * For example, if the unparsed address was + * + * <"Joe Cheng"@joecheng.com> + * + * this method would return + * + * <Joe Cheng@joecheng.com> + * + * which is not valid for transport; the local part + * would need to be re-quoted. + * + * @param includeRoute true if the route should be included if it exists. + */ + public String getAddressString(boolean includeRoute) { + return "<" + (!includeRoute || route == null ? "" : route.toRouteString() + ":") + + localPart + + (domain == null ? "" : "@") + + domain + ">"; + } + + @Override + protected final void doAddMailboxesTo(ArrayList<Address> results) { + results.add(this); + } + + @Override + public String toString() { + return getAddressString(); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java new file mode 100644 index 000000000..2c9efb37f --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java @@ -0,0 +1,71 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field.address; + +import java.util.ArrayList; + +/** + * An immutable, random-access list of Mailbox objects. + * + * + */ +public class MailboxList { + + private ArrayList<Address> mailboxes; + + /** + * @param mailboxes An ArrayList that contains only Mailbox objects. + * @param dontCopy true iff it is not possible for the mailboxes ArrayList to be modified by someone else. + */ + public MailboxList(ArrayList<Address> mailboxes, boolean dontCopy) { + if (mailboxes != null) + this.mailboxes = (dontCopy ? mailboxes : new ArrayList<Address>(mailboxes)); + else + this.mailboxes = new ArrayList<Address>(0); + } + + /** + * The number of elements in this list. + */ + public int size() { + return mailboxes.size(); + } + + /** + * Gets an address. + */ + public Mailbox get(int index) { + if (0 > index || size() <= index) + throw new IndexOutOfBoundsException(); + return (Mailbox)mailboxes.get(index); + } + + /** + * Dumps a representation of this mailbox list to + * stdout, for debugging purposes. + */ + public void print() { + for (int i = 0; i < size(); i++) { + Mailbox mailbox = get(i); + System.out.println(mailbox.toString()); + } + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java new file mode 100644 index 000000000..4b8306037 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java @@ -0,0 +1,71 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field.address; + +/** + * A Mailbox that has a name/description. + * + * + */ +public class NamedMailbox extends Mailbox { + private String name; + + /** + * @see Mailbox#Mailbox(String, String) + */ + public NamedMailbox(String name, String localPart, String domain) { + super(localPart, domain); + this.name = name; + } + + /** + * @see Mailbox#Mailbox(DomainList, String, String) + */ + public NamedMailbox(String name, DomainList route, String localPart, String domain) { + super(route, localPart, domain); + this.name = name; + } + + /** + * Creates a named mailbox based on an unnamed mailbox. + */ + public NamedMailbox(String name, Mailbox baseMailbox) { + super(baseMailbox.getRoute(), baseMailbox.getLocalPart(), baseMailbox.getDomain()); + this.name = name; + } + + /** + * Returns the name of the mailbox. + */ + public String getName() { + return this.name; + } + + /** + * Same features (or problems) as Mailbox.getAddressString(boolean), + * only more so. + * + * @see Mailbox#getAddressString(boolean) + */ + @Override + public String getAddressString(boolean includeRoute) { + return (name == null ? "" : name + " ") + super.getAddressString(includeRoute); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java new file mode 100644 index 000000000..4d56d000b --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTaddr_spec.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTaddr_spec extends SimpleNode { + public ASTaddr_spec(int id) { + super(id); + } + + public ASTaddr_spec(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java new file mode 100644 index 000000000..47bdeda8e --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTaddress.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTaddress extends SimpleNode { + public ASTaddress(int id) { + super(id); + } + + public ASTaddress(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java new file mode 100644 index 000000000..737840e38 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTaddress_list.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTaddress_list extends SimpleNode { + public ASTaddress_list(int id) { + super(id); + } + + public ASTaddress_list(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java new file mode 100644 index 000000000..8cb8f421f --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTangle_addr.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTangle_addr extends SimpleNode { + public ASTangle_addr(int id) { + super(id); + } + + public ASTangle_addr(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java new file mode 100644 index 000000000..b52664386 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTdomain.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTdomain extends SimpleNode { + public ASTdomain(int id) { + super(id); + } + + public ASTdomain(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java new file mode 100644 index 000000000..f6017b9fc --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTgroup_body.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTgroup_body extends SimpleNode { + public ASTgroup_body(int id) { + super(id); + } + + public ASTgroup_body(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java new file mode 100644 index 000000000..5c244fa3e --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTlocal_part.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTlocal_part extends SimpleNode { + public ASTlocal_part(int id) { + super(id); + } + + public ASTlocal_part(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java new file mode 100644 index 000000000..aeb469da1 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTmailbox.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTmailbox extends SimpleNode { + public ASTmailbox(int id) { + super(id); + } + + public ASTmailbox(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java new file mode 100644 index 000000000..846c73167 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTname_addr.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTname_addr extends SimpleNode { + public ASTname_addr(int id) { + super(id); + } + + public ASTname_addr(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java new file mode 100644 index 000000000..7d711c529 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTphrase.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTphrase extends SimpleNode { + public ASTphrase(int id) { + super(id); + } + + public ASTphrase(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java new file mode 100644 index 000000000..54ea11523 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. ASTroute.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class ASTroute extends SimpleNode { + public ASTroute(int id) { + super(id); + } + + public ASTroute(AddressListParser p, int id) { + super(p, id); + } + + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java new file mode 100644 index 000000000..8094df0ad --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java @@ -0,0 +1,977 @@ +/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParser.java */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.address.parser; + +public class AddressListParser/*@bgen(jjtree)*/implements AddressListParserTreeConstants, AddressListParserConstants {/*@bgen(jjtree)*/ + protected JJTAddressListParserState jjtree = new JJTAddressListParserState();public static void main(String args[]) throws ParseException { + while (true) { + try { + AddressListParser parser = new AddressListParser(System.in); + parser.parseLine(); + ((SimpleNode)parser.jjtree.rootNode()).dump("> "); + } catch (Exception x) { + x.printStackTrace(); + return; + } + } + } + + private static void log(String msg) { + System.out.print(msg); + } + + public ASTaddress_list parse() throws ParseException { + try { + parseAll(); + return (ASTaddress_list)jjtree.rootNode(); + } catch (TokenMgrError tme) { + throw new ParseException(tme.getMessage()); + } + } + + + void jjtreeOpenNodeScope(Node n) { + ((SimpleNode)n).firstToken = getToken(1); + } + + void jjtreeCloseNodeScope(Node n) { + ((SimpleNode)n).lastToken = getToken(0); + } + + final public void parseLine() throws ParseException { + address_list(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 1: + jj_consume_token(1); + break; + default: + jj_la1[0] = jj_gen; + ; + } + jj_consume_token(2); + } + + final public void parseAll() throws ParseException { + address_list(); + jj_consume_token(0); + } + + final public void address_list() throws ParseException { + /*@bgen(jjtree) address_list */ + ASTaddress_list jjtn000 = new ASTaddress_list(JJTADDRESS_LIST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + case DOTATOM: + case QUOTEDSTRING: + address(); + break; + default: + jj_la1[1] = jj_gen; + ; + } + label_1: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 3: + ; + break; + default: + jj_la1[2] = jj_gen; + break label_1; + } + jj_consume_token(3); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + case DOTATOM: + case QUOTEDSTRING: + address(); + break; + default: + jj_la1[3] = jj_gen; + ; + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void address() throws ParseException { + /*@bgen(jjtree) address */ + ASTaddress jjtn000 = new ASTaddress(JJTADDRESS); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + if (jj_2_1(2147483647)) { + addr_spec(); + } else { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + angle_addr(); + break; + case DOTATOM: + case QUOTEDSTRING: + phrase(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 4: + group_body(); + break; + case 6: + angle_addr(); + break; + default: + jj_la1[4] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + break; + default: + jj_la1[5] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void mailbox() throws ParseException { + /*@bgen(jjtree) mailbox */ + ASTmailbox jjtn000 = new ASTmailbox(JJTMAILBOX); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + if (jj_2_2(2147483647)) { + addr_spec(); + } else { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + angle_addr(); + break; + case DOTATOM: + case QUOTEDSTRING: + name_addr(); + break; + default: + jj_la1[6] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void name_addr() throws ParseException { + /*@bgen(jjtree) name_addr */ + ASTname_addr jjtn000 = new ASTname_addr(JJTNAME_ADDR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + phrase(); + angle_addr(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void group_body() throws ParseException { + /*@bgen(jjtree) group_body */ + ASTgroup_body jjtn000 = new ASTgroup_body(JJTGROUP_BODY); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + jj_consume_token(4); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + case DOTATOM: + case QUOTEDSTRING: + mailbox(); + break; + default: + jj_la1[7] = jj_gen; + ; + } + label_2: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 3: + ; + break; + default: + jj_la1[8] = jj_gen; + break label_2; + } + jj_consume_token(3); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 6: + case DOTATOM: + case QUOTEDSTRING: + mailbox(); + break; + default: + jj_la1[9] = jj_gen; + ; + } + } + jj_consume_token(5); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void angle_addr() throws ParseException { + /*@bgen(jjtree) angle_addr */ + ASTangle_addr jjtn000 = new ASTangle_addr(JJTANGLE_ADDR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + jj_consume_token(6); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 8: + route(); + break; + default: + jj_la1[10] = jj_gen; + ; + } + addr_spec(); + jj_consume_token(7); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void route() throws ParseException { + /*@bgen(jjtree) route */ + ASTroute jjtn000 = new ASTroute(JJTROUTE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + jj_consume_token(8); + domain(); + label_3: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 3: + case 8: + ; + break; + default: + jj_la1[11] = jj_gen; + break label_3; + } + label_4: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 3: + ; + break; + default: + jj_la1[12] = jj_gen; + break label_4; + } + jj_consume_token(3); + } + jj_consume_token(8); + domain(); + } + jj_consume_token(4); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void phrase() throws ParseException { + /*@bgen(jjtree) phrase */ + ASTphrase jjtn000 = new ASTphrase(JJTPHRASE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + label_5: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DOTATOM: + jj_consume_token(DOTATOM); + break; + case QUOTEDSTRING: + jj_consume_token(QUOTEDSTRING); + break; + default: + jj_la1[13] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DOTATOM: + case QUOTEDSTRING: + ; + break; + default: + jj_la1[14] = jj_gen; + break label_5; + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void addr_spec() throws ParseException { + /*@bgen(jjtree) addr_spec */ + ASTaddr_spec jjtn000 = new ASTaddr_spec(JJTADDR_SPEC); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); + try { + local_part(); + jj_consume_token(8); + domain(); + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + {if (true) throw (RuntimeException)jjte000;} + } + if (jjte000 instanceof ParseException) { + {if (true) throw (ParseException)jjte000;} + } + {if (true) throw (Error)jjte000;} + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void local_part() throws ParseException { + /*@bgen(jjtree) local_part */ + ASTlocal_part jjtn000 = new ASTlocal_part(JJTLOCAL_PART); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000);Token t; + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DOTATOM: + t = jj_consume_token(DOTATOM); + break; + case QUOTEDSTRING: + t = jj_consume_token(QUOTEDSTRING); + break; + default: + jj_la1[15] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + label_6: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 9: + case DOTATOM: + case QUOTEDSTRING: + ; + break; + default: + jj_la1[16] = jj_gen; + break label_6; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 9: + t = jj_consume_token(9); + break; + default: + jj_la1[17] = jj_gen; + ; + } + if (t.image.charAt(t.image.length() - 1) != '.' || t.kind == AddressListParserConstants.QUOTEDSTRING) + {if (true) throw new ParseException("Words in local part must be separated by '.'");} + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DOTATOM: + t = jj_consume_token(DOTATOM); + break; + case QUOTEDSTRING: + t = jj_consume_token(QUOTEDSTRING); + break; + default: + jj_la1[18] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final public void domain() throws ParseException { + /*@bgen(jjtree) domain */ + ASTdomain jjtn000 = new ASTdomain(JJTDOMAIN); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000);Token t; + try { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case DOTATOM: + t = jj_consume_token(DOTATOM); + label_7: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 9: + case DOTATOM: + ; + break; + default: + jj_la1[19] = jj_gen; + break label_7; + } + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 9: + t = jj_consume_token(9); + break; + default: + jj_la1[20] = jj_gen; + ; + } + if (t.image.charAt(t.image.length() - 1) != '.') + {if (true) throw new ParseException("Atoms in domain names must be separated by '.'");} + t = jj_consume_token(DOTATOM); + } + break; + case DOMAINLITERAL: + jj_consume_token(DOMAINLITERAL); + break; + default: + jj_la1[21] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } + } + + final private boolean jj_2_1(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_1(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(0, xla); } + } + + final private boolean jj_2_2(int xla) { + jj_la = xla; jj_lastpos = jj_scanpos = token; + try { return !jj_3_2(); } + catch(LookaheadSuccess ls) { return true; } + finally { jj_save(1, xla); } + } + + final private boolean jj_3R_11() { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(9)) jj_scanpos = xsp; + xsp = jj_scanpos; + if (jj_scan_token(14)) { + jj_scanpos = xsp; + if (jj_scan_token(31)) return true; + } + return false; + } + + final private boolean jj_3R_13() { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(9)) jj_scanpos = xsp; + if (jj_scan_token(DOTATOM)) return true; + return false; + } + + final private boolean jj_3R_8() { + if (jj_3R_9()) return true; + if (jj_scan_token(8)) return true; + if (jj_3R_10()) return true; + return false; + } + + final private boolean jj_3_1() { + if (jj_3R_8()) return true; + return false; + } + + final private boolean jj_3R_12() { + if (jj_scan_token(DOTATOM)) return true; + Token xsp; + while (true) { + xsp = jj_scanpos; + if (jj_3R_13()) { jj_scanpos = xsp; break; } + } + return false; + } + + final private boolean jj_3R_10() { + Token xsp; + xsp = jj_scanpos; + if (jj_3R_12()) { + jj_scanpos = xsp; + if (jj_scan_token(18)) return true; + } + return false; + } + + final private boolean jj_3_2() { + if (jj_3R_8()) return true; + return false; + } + + final private boolean jj_3R_9() { + Token xsp; + xsp = jj_scanpos; + if (jj_scan_token(14)) { + jj_scanpos = xsp; + if (jj_scan_token(31)) return true; + } + while (true) { + xsp = jj_scanpos; + if (jj_3R_11()) { jj_scanpos = xsp; break; } + } + return false; + } + + public AddressListParserTokenManager token_source; + SimpleCharStream jj_input_stream; + public Token token, jj_nt; + private int jj_ntk; + private Token jj_scanpos, jj_lastpos; + private int jj_la; + public boolean lookingAhead = false; + private boolean jj_semLA; + private int jj_gen; + final private int[] jj_la1 = new int[22]; + static private int[] jj_la1_0; + static private int[] jj_la1_1; + static { + jj_la1_0(); + jj_la1_1(); + } + private static void jj_la1_0() { + jj_la1_0 = new int[] {0x2,0x80004040,0x8,0x80004040,0x50,0x80004040,0x80004040,0x80004040,0x8,0x80004040,0x100,0x108,0x8,0x80004000,0x80004000,0x80004000,0x80004200,0x200,0x80004000,0x4200,0x200,0x44000,}; + } + private static void jj_la1_1() { + jj_la1_1 = new int[] {0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,0x0,}; + } + final private JJCalls[] jj_2_rtns = new JJCalls[2]; + private boolean jj_rescan = false; + private int jj_gc = 0; + + public AddressListParser(java.io.InputStream stream) { + this(stream, null); + } + public AddressListParser(java.io.InputStream stream, String encoding) { + try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source = new AddressListParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + public void ReInit(java.io.InputStream stream, String encoding) { + try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public AddressListParser(java.io.Reader stream) { + jj_input_stream = new SimpleCharStream(stream, 1, 1); + token_source = new AddressListParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public void ReInit(java.io.Reader stream) { + jj_input_stream.ReInit(stream, 1, 1); + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public AddressListParser(AddressListParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + public void ReInit(AddressListParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jjtree.reset(); + jj_gen = 0; + for (int i = 0; i < 22; i++) jj_la1[i] = -1; + for (int i = 0; i < jj_2_rtns.length; i++) jj_2_rtns[i] = new JJCalls(); + } + + final private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + if (++jj_gc > 100) { + jj_gc = 0; + for (int i = 0; i < jj_2_rtns.length; i++) { + JJCalls c = jj_2_rtns[i]; + while (c != null) { + if (c.gen < jj_gen) c.first = null; + c = c.next; + } + } + } + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + static private final class LookaheadSuccess extends java.lang.Error { } + final private LookaheadSuccess jj_ls = new LookaheadSuccess(); + final private boolean jj_scan_token(int kind) { + if (jj_scanpos == jj_lastpos) { + jj_la--; + if (jj_scanpos.next == null) { + jj_lastpos = jj_scanpos = jj_scanpos.next = token_source.getNextToken(); + } else { + jj_lastpos = jj_scanpos = jj_scanpos.next; + } + } else { + jj_scanpos = jj_scanpos.next; + } + if (jj_rescan) { + int i = 0; Token tok = token; + while (tok != null && tok != jj_scanpos) { i++; tok = tok.next; } + if (tok != null) jj_add_error_token(kind, i); + } + if (jj_scanpos.kind != kind) return true; + if (jj_la == 0 && jj_scanpos == jj_lastpos) throw jj_ls; + return false; + } + + final public Token getNextToken() { + if (token.next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + jj_gen++; + return token; + } + + final public Token getToken(int index) { + Token t = lookingAhead ? jj_scanpos : token; + for (int i = 0; i < index; i++) { + if (t.next != null) t = t.next; + else t = t.next = token_source.getNextToken(); + } + return t; + } + + final private int jj_ntk() { + if ((jj_nt=token.next) == null) + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + else + return (jj_ntk = jj_nt.kind); + } + + private java.util.Vector<int[]> jj_expentries = new java.util.Vector<int[]>(); + private int[] jj_expentry; + private int jj_kind = -1; + private int[] jj_lasttokens = new int[100]; + private int jj_endpos; + + private void jj_add_error_token(int kind, int pos) { + if (pos >= 100) return; + if (pos == jj_endpos + 1) { + jj_lasttokens[jj_endpos++] = kind; + } else if (jj_endpos != 0) { + jj_expentry = new int[jj_endpos]; + for (int i = 0; i < jj_endpos; i++) { + jj_expentry[i] = jj_lasttokens[i]; + } + boolean exists = false; + for (java.util.Enumeration<int[]> e = jj_expentries.elements(); e.hasMoreElements();) { + int[] oldentry = e.nextElement(); + if (oldentry.length == jj_expentry.length) { + exists = true; + for (int i = 0; i < jj_expentry.length; i++) { + if (oldentry[i] != jj_expentry[i]) { + exists = false; + break; + } + } + if (exists) break; + } + } + if (!exists) jj_expentries.addElement(jj_expentry); + if (pos != 0) jj_lasttokens[(jj_endpos = pos) - 1] = kind; + } + } + + public ParseException generateParseException() { + jj_expentries.removeAllElements(); + boolean[] la1tokens = new boolean[34]; + for (int i = 0; i < 34; i++) { + la1tokens[i] = false; + } + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 22; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1<<j)) != 0) { + la1tokens[j] = true; + } + if ((jj_la1_1[i] & (1<<j)) != 0) { + la1tokens[32+j] = true; + } + } + } + } + for (int i = 0; i < 34; i++) { + if (la1tokens[i]) { + jj_expentry = new int[1]; + jj_expentry[0] = i; + jj_expentries.addElement(jj_expentry); + } + } + jj_endpos = 0; + jj_rescan_token(); + jj_add_error_token(0, 0); + int[][] exptokseq = new int[jj_expentries.size()][]; + for (int i = 0; i < jj_expentries.size(); i++) { + exptokseq[i] = jj_expentries.elementAt(i); + } + return new ParseException(token, exptokseq, tokenImage); + } + + final public void enable_tracing() { + } + + final public void disable_tracing() { + } + + final private void jj_rescan_token() { + jj_rescan = true; + for (int i = 0; i < 2; i++) { + try { + JJCalls p = jj_2_rtns[i]; + do { + if (p.gen > jj_gen) { + jj_la = p.arg; jj_lastpos = jj_scanpos = p.first; + switch (i) { + case 0: jj_3_1(); break; + case 1: jj_3_2(); break; + } + } + p = p.next; + } while (p != null); + } catch(LookaheadSuccess ls) { } + } + jj_rescan = false; + } + + final private void jj_save(int index, int xla) { + JJCalls p = jj_2_rtns[index]; + while (p.gen > jj_gen) { + if (p.next == null) { p = p.next = new JJCalls(); break; } + p = p.next; + } + p.gen = jj_gen + xla - jj_la; p.first = token; p.arg = xla; + } + + static final class JJCalls { + int gen; + Token first; + int arg; + JJCalls next; + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj new file mode 100644 index 000000000..c14277bc6 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj @@ -0,0 +1,595 @@ +/*@bgen(jjtree) Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParser.jj */ +/*@egen*//**************************************************************** + * 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. * + ****************************************************************/ + + +/** + * RFC2822 address list parser. + * + * Created 9/17/2004 + * by Joe Cheng <code@joecheng.com> + */ + +options { + STATIC=false; + LOOKAHEAD=1; + //DEBUG_PARSER=true; + //DEBUG_TOKEN_MANAGER=true; +} + +PARSER_BEGIN(AddressListParser) +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.address.parser; + +public class AddressListParser/*@bgen(jjtree)*/implements AddressListParserTreeConstants/*@egen*/ {/*@bgen(jjtree)*/ + protected JJTAddressListParserState jjtree = new JJTAddressListParserState(); + +/*@egen*/ + public static void main(String args[]) throws ParseException { + while (true) { + try { + AddressListParser parser = new AddressListParser(System.in); + parser.parseLine(); + ((SimpleNode)parser.jjtree.rootNode()).dump("> "); + } catch (Exception x) { + x.printStackTrace(); + return; + } + } + } + + private static void log(String msg) { + System.out.print(msg); + } + + public ASTaddress_list parse() throws ParseException { + try { + parseAll(); + return (ASTaddress_list)jjtree.rootNode(); + } catch (TokenMgrError tme) { + throw new ParseException(tme.getMessage()); + } + } + + + void jjtreeOpenNodeScope(Node n) { + ((SimpleNode)n).firstToken = getToken(1); + } + + void jjtreeCloseNodeScope(Node n) { + ((SimpleNode)n).lastToken = getToken(0); + } +} + +PARSER_END(AddressListParser) + +void parseLine() : +{} +{ + address_list() ["\r"] "\n" +} + +void parseAll() : +{} +{ + address_list() <EOF> +} + +void address_list() : +{/*@bgen(jjtree) address_list */ + ASTaddress_list jjtn000 = new ASTaddress_list(JJTADDRESS_LIST); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) address_list */ + try { +/*@egen*/ + [ address() ] + ( + "," + [ address() ] + )*/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void address() : +{/*@bgen(jjtree) address */ + ASTaddress jjtn000 = new ASTaddress(JJTADDRESS); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) address */ + try { +/*@egen*/ + LOOKAHEAD(2147483647) + addr_spec() +| angle_addr() +| ( phrase() (group_body() | angle_addr()) )/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void mailbox() : +{/*@bgen(jjtree) mailbox */ + ASTmailbox jjtn000 = new ASTmailbox(JJTMAILBOX); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) mailbox */ + try { +/*@egen*/ + LOOKAHEAD(2147483647) + addr_spec() +| angle_addr() +| name_addr()/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void name_addr() : +{/*@bgen(jjtree) name_addr */ + ASTname_addr jjtn000 = new ASTname_addr(JJTNAME_ADDR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) name_addr */ + try { +/*@egen*/ + phrase() angle_addr()/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void group_body() : +{/*@bgen(jjtree) group_body */ + ASTgroup_body jjtn000 = new ASTgroup_body(JJTGROUP_BODY); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) group_body */ + try { +/*@egen*/ + ":" + [ mailbox() ] + ( + "," + [ mailbox() ] + )* + ";"/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void angle_addr() : +{/*@bgen(jjtree) angle_addr */ + ASTangle_addr jjtn000 = new ASTangle_addr(JJTANGLE_ADDR); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) angle_addr */ + try { +/*@egen*/ + "<" [ route() ] addr_spec() ">"/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void route() : +{/*@bgen(jjtree) route */ + ASTroute jjtn000 = new ASTroute(JJTROUTE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) route */ + try { +/*@egen*/ + "@" domain() ( (",")* "@" domain() )* ":"/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void phrase() : +{/*@bgen(jjtree) phrase */ + ASTphrase jjtn000 = new ASTphrase(JJTPHRASE); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) phrase */ +try { +/*@egen*/ +( <DOTATOM> +| <QUOTEDSTRING> +)+/*@bgen(jjtree)*/ +} finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } +} +/*@egen*/ +} + +void addr_spec() : +{/*@bgen(jjtree) addr_spec */ + ASTaddr_spec jjtn000 = new ASTaddr_spec(JJTADDR_SPEC); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/} +{/*@bgen(jjtree) addr_spec */ + try { +/*@egen*/ + ( local_part() "@" domain() )/*@bgen(jjtree)*/ + } catch (Throwable jjte000) { + if (jjtc000) { + jjtree.clearNodeScope(jjtn000); + jjtc000 = false; + } else { + jjtree.popNode(); + } + if (jjte000 instanceof RuntimeException) { + throw (RuntimeException)jjte000; + } + if (jjte000 instanceof ParseException) { + throw (ParseException)jjte000; + } + throw (Error)jjte000; + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void local_part() : +{/*@bgen(jjtree) local_part */ + ASTlocal_part jjtn000 = new ASTlocal_part(JJTLOCAL_PART); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/ Token t; } +{/*@bgen(jjtree) local_part */ + try { +/*@egen*/ + ( t=<DOTATOM> | t=<QUOTEDSTRING> ) + ( [t="."] + { + if (t.image.charAt(t.image.length() - 1) != '.' || t.kind == AddressListParserConstants.QUOTEDSTRING) + throw new ParseException("Words in local part must be separated by '.'"); + } + ( t=<DOTATOM> | t=<QUOTEDSTRING> ) + )*/*@bgen(jjtree)*/ + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +void domain() : +{/*@bgen(jjtree) domain */ + ASTdomain jjtn000 = new ASTdomain(JJTDOMAIN); + boolean jjtc000 = true; + jjtree.openNodeScope(jjtn000); + jjtreeOpenNodeScope(jjtn000); +/*@egen*/ Token t; } +{/*@bgen(jjtree) domain */ + try { +/*@egen*/ + ( t=<DOTATOM> + ( [t="."] + { + if (t.image.charAt(t.image.length() - 1) != '.') + throw new ParseException("Atoms in domain names must be separated by '.'"); + } + t=<DOTATOM> + )* + ) +| <DOMAINLITERAL>/*@bgen(jjtree)*/ + } finally { + if (jjtc000) { + jjtree.closeNodeScope(jjtn000, true); + jjtreeCloseNodeScope(jjtn000); + } + } +/*@egen*/ +} + +SPECIAL_TOKEN : +{ + < WS: ( [" ", "\t"] )+ > +} + +TOKEN : +{ + < #ALPHA: ["a" - "z", "A" - "Z"] > +| < #DIGIT: ["0" - "9"] > +| < #ATEXT: ( <ALPHA> | <DIGIT> + | "!" | "#" | "$" | "%" + | "&" | "'" | "*" | "+" + | "-" | "/" | "=" | "?" + | "^" | "_" | "`" | "{" + | "|" | "}" | "~" + )> +| < DOTATOM: <ATEXT> ( <ATEXT> | "." )* > +} + +TOKEN_MGR_DECLS : +{ + // Keeps track of how many levels of comment nesting + // we've encountered. This is only used when the 2nd + // level is reached, for example ((this)), not (this). + // This is because the outermost level must be treated + // specially anyway, because the outermost ")" has a + // different token type than inner ")" instances. + static int commentNest; +} + +MORE : +{ + // domain literal + "[" : INDOMAINLITERAL +} + +<INDOMAINLITERAL> +MORE : +{ + < <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); } +| < ~["[", "]", "\\"] > +} + +<INDOMAINLITERAL> +TOKEN : +{ + < DOMAINLITERAL: "]" > { matchedToken.image = image.toString(); }: DEFAULT +} + +MORE : +{ + // starts a comment + "(" : INCOMMENT +} + +<INCOMMENT> +SKIP : +{ + // ends a comment + < COMMENT: ")" > : DEFAULT + // if this is ever changed to not be a SKIP, need + // to make sure matchedToken.token = token.toString() + // is called. +} + +<INCOMMENT> +MORE : +{ + < <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); } +| "(" { commentNest = 1; } : NESTED_COMMENT +| < <ANY>> +} + +<NESTED_COMMENT> +MORE : +{ + < <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); } +| "(" { ++commentNest; } +| ")" { --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT); } +| < <ANY>> +} + + +// QUOTED STRINGS + +MORE : +{ + "\"" { image.deleteCharAt(image.length() - 1); } : INQUOTEDSTRING +} + +<INQUOTEDSTRING> +MORE : +{ + < <QUOTEDPAIR>> { image.deleteCharAt(image.length() - 2); } +| < (~["\"", "\\"])+ > +} + +<INQUOTEDSTRING> +TOKEN : +{ + < QUOTEDSTRING: "\"" > { matchedToken.image = image.substring(0, image.length() - 1); } : DEFAULT +} + +// GLOBALS + +<*> +TOKEN : +{ + < #QUOTEDPAIR: "\\" <ANY> > +| < #ANY: ~[] > +} + +// ERROR! +/* + +<*> +TOKEN : +{ + < UNEXPECTED_CHAR: <ANY> > +} + +*/
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java new file mode 100644 index 000000000..006a082c1 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java @@ -0,0 +1,76 @@ +/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParserConstants.java */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.address.parser; + +public interface AddressListParserConstants { + + int EOF = 0; + int WS = 10; + int ALPHA = 11; + int DIGIT = 12; + int ATEXT = 13; + int DOTATOM = 14; + int DOMAINLITERAL = 18; + int COMMENT = 20; + int QUOTEDSTRING = 31; + int QUOTEDPAIR = 32; + int ANY = 33; + + int DEFAULT = 0; + int INDOMAINLITERAL = 1; + int INCOMMENT = 2; + int NESTED_COMMENT = 3; + int INQUOTEDSTRING = 4; + + String[] tokenImage = { + "<EOF>", + "\"\\r\"", + "\"\\n\"", + "\",\"", + "\":\"", + "\";\"", + "\"<\"", + "\">\"", + "\"@\"", + "\".\"", + "<WS>", + "<ALPHA>", + "<DIGIT>", + "<ATEXT>", + "<DOTATOM>", + "\"[\"", + "<token of kind 16>", + "<token of kind 17>", + "\"]\"", + "\"(\"", + "\")\"", + "<token of kind 21>", + "\"(\"", + "<token of kind 23>", + "<token of kind 24>", + "\"(\"", + "\")\"", + "<token of kind 27>", + "\"\\\"\"", + "<token of kind 29>", + "<token of kind 30>", + "\"\\\"\"", + "<QUOTEDPAIR>", + "<ANY>", + }; + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java new file mode 100644 index 000000000..d2dd88dd3 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java @@ -0,0 +1,1009 @@ +/* Generated By:JJTree&JavaCC: Do not edit this line. AddressListParserTokenManager.java */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.address.parser; + +public class AddressListParserTokenManager implements AddressListParserConstants +{ + // Keeps track of how many levels of comment nesting + // we've encountered. This is only used when the 2nd + // level is reached, for example ((this)), not (this). + // This is because the outermost level must be treated + // specially anyway, because the outermost ")" has a + // different token type than inner ")" instances. + static int commentNest; + public java.io.PrintStream debugStream = System.out; + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_0(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_0(int pos, long active0) +{ + return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1); +} +private final int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private final int jjStartNfaWithStates_0(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_0(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_0() +{ + switch(curChar) + { + case 10: + return jjStopAtPos(0, 2); + case 13: + return jjStopAtPos(0, 1); + case 34: + return jjStopAtPos(0, 28); + case 40: + return jjStopAtPos(0, 19); + case 44: + return jjStopAtPos(0, 3); + case 46: + return jjStopAtPos(0, 9); + case 58: + return jjStopAtPos(0, 4); + case 59: + return jjStopAtPos(0, 5); + case 60: + return jjStopAtPos(0, 6); + case 62: + return jjStopAtPos(0, 7); + case 64: + return jjStopAtPos(0, 8); + case 91: + return jjStopAtPos(0, 15); + default : + return jjMoveNfa_0(1, 0); + } +} +private final void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private final void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private final void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} +private final void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} +private final void jjCheckNAddStates(int start) +{ + jjCheckNAdd(jjnextStates[start]); + jjCheckNAdd(jjnextStates[start + 1]); +} +private final int jjMoveNfa_0(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + if ((0xa3ffacfa00000000L & l) != 0L) + { + if (kind > 14) + kind = 14; + jjCheckNAdd(2); + } + else if ((0x100000200L & l) != 0L) + { + if (kind > 10) + kind = 10; + jjCheckNAdd(0); + } + break; + case 0: + if ((0x100000200L & l) == 0L) + break; + kind = 10; + jjCheckNAdd(0); + break; + case 2: + if ((0xa3ffecfa00000000L & l) == 0L) + break; + if (kind > 14) + kind = 14; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 1: + case 2: + if ((0x7fffffffc7fffffeL & l) == 0L) + break; + if (kind > 14) + kind = 14; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_2(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_2(int pos, long active0) +{ + return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_2(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_2(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_2() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 22); + case 41: + return jjStopAtPos(0, 20); + default : + return jjMoveNfa_2(0, 0); + } +} +static final long[] jjbitVec0 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private final int jjMoveNfa_2(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 23) + kind = 23; + break; + case 1: + if (kind > 21) + kind = 21; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 23) + kind = 23; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 21) + kind = 21; + break; + case 2: + if (kind > 23) + kind = 23; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 23) + kind = 23; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 21) + kind = 21; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_4(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_4(int pos, long active0) +{ + return jjMoveNfa_4(jjStopStringLiteralDfa_4(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_4(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_4(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_4() +{ + switch(curChar) + { + case 34: + return jjStopAtPos(0, 31); + default : + return jjMoveNfa_4(0, 0); + } +} +private final int jjMoveNfa_4(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 2: + if ((0xfffffffbffffffffL & l) == 0L) + break; + if (kind > 30) + kind = 30; + jjCheckNAdd(2); + break; + case 1: + if (kind > 29) + kind = 29; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffffefffffffL & l) != 0L) + { + if (kind > 30) + kind = 30; + jjCheckNAdd(2); + } + else if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 29) + kind = 29; + break; + case 2: + if ((0xffffffffefffffffL & l) == 0L) + break; + if (kind > 30) + kind = 30; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 2: + if ((jjbitVec0[i2] & l2) == 0L) + break; + if (kind > 30) + kind = 30; + jjCheckNAdd(2); + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 29) + kind = 29; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_3(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_3(int pos, long active0) +{ + return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_3(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_3(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_3() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 25); + case 41: + return jjStopAtPos(0, 26); + default : + return jjMoveNfa_3(0, 0); + } +} +private final int jjMoveNfa_3(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 27) + kind = 27; + break; + case 1: + if (kind > 24) + kind = 24; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 27) + kind = 27; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 24) + kind = 24; + break; + case 2: + if (kind > 27) + kind = 27; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 27) + kind = 27; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 24) + kind = 24; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_1(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_1(int pos, long active0) +{ + return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_1(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_1(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_1() +{ + switch(curChar) + { + case 93: + return jjStopAtPos(0, 18); + default : + return jjMoveNfa_1(0, 0); + } +} +private final int jjMoveNfa_1(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 17) + kind = 17; + break; + case 1: + if (kind > 16) + kind = 16; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffffc7ffffffL & l) != 0L) + { + if (kind > 17) + kind = 17; + } + else if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 16) + kind = 16; + break; + case 2: + if ((0xffffffffc7ffffffL & l) != 0L && kind > 17) + kind = 17; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 17) + kind = 17; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 16) + kind = 16; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +static final int[] jjnextStates = { +}; +public static final String[] jjstrLiteralImages = { +"", "\15", "\12", "\54", "\72", "\73", "\74", "\76", "\100", "\56", null, null, +null, null, null, null, null, null, null, null, null, null, null, null, null, null, +null, null, null, null, null, null, null, null, }; +public static final String[] lexStateNames = { + "DEFAULT", + "INDOMAINLITERAL", + "INCOMMENT", + "NESTED_COMMENT", + "INQUOTEDSTRING", +}; +public static final int[] jjnewLexState = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1, -1, -1, 0, 2, 0, -1, 3, -1, -1, + -1, -1, -1, 4, -1, -1, 0, -1, -1, +}; +static final long[] jjtoToken = { + 0x800443ffL, +}; +static final long[] jjtoSkip = { + 0x100400L, +}; +static final long[] jjtoSpecial = { + 0x400L, +}; +static final long[] jjtoMore = { + 0x7feb8000L, +}; +protected SimpleCharStream input_stream; +private final int[] jjrounds = new int[3]; +private final int[] jjstateSet = new int[6]; +StringBuffer image; +int jjimageLen; +int lengthOfMatch; +protected char curChar; +public AddressListParserTokenManager(SimpleCharStream stream){ + if (SimpleCharStream.staticFlag) + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + input_stream = stream; +} +public AddressListParserTokenManager(SimpleCharStream stream, int lexState){ + this(stream); + SwitchTo(lexState); +} +public void ReInit(SimpleCharStream stream) +{ + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); +} +private final void ReInitRounds() +{ + int i; + jjround = 0x80000001; + for (i = 3; i-- > 0;) + jjrounds[i] = 0x80000000; +} +public void ReInit(SimpleCharStream stream, int lexState) +{ + ReInit(stream); + SwitchTo(lexState); +} +public void SwitchTo(int lexState) +{ + if (lexState >= 5 || lexState < 0) + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + else + curLexState = lexState; +} + +protected Token jjFillToken() +{ + Token t = Token.newToken(jjmatchedKind); + t.kind = jjmatchedKind; + String im = jjstrLiteralImages[jjmatchedKind]; + t.image = (im == null) ? input_stream.GetImage() : im; + t.beginLine = input_stream.getBeginLine(); + t.beginColumn = input_stream.getBeginColumn(); + t.endLine = input_stream.getEndLine(); + t.endColumn = input_stream.getEndColumn(); + return t; +} + +int curLexState = 0; +int defaultLexState = 0; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +public Token getNextToken() +{ + int kind; + Token specialToken = null; + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(java.io.IOException e) + { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + return matchedToken; + } + image = null; + jjimageLen = 0; + + for (;;) + { + switch(curLexState) + { + case 0: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + break; + case 1: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_1(); + break; + case 2: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_2(); + break; + case 3: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_3(); + break; + case 4: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_4(); + break; + } + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) + input_stream.backup(curPos - jjmatchedPos - 1); + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + TokenLexicalActions(matchedToken); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + return matchedToken; + } + else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + if (specialToken == null) + specialToken = matchedToken; + else + { + matchedToken.specialToken = specialToken; + specialToken = (specialToken.next = matchedToken); + } + } + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + continue EOFLoop; + } + MoreLexicalActions(); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + curPos = 0; + jjmatchedKind = 0x7fffffff; + try { + curChar = input_stream.readChar(); + continue; + } + catch (java.io.IOException e1) { } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } + else + error_column++; + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } + } +} + +void MoreLexicalActions() +{ + jjimageLen += (lengthOfMatch = jjmatchedPos + 1); + switch(jjmatchedKind) + { + case 16 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 21 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 22 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + commentNest = 1; + break; + case 24 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 25 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + ++commentNest; + break; + case 26 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT); + break; + case 28 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 1); + break; + case 29 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + default : + break; + } +} +void TokenLexicalActions(Token matchedToken) +{ + switch(jjmatchedKind) + { + case 18 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + matchedToken.image = image.toString(); + break; + case 31 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + matchedToken.image = image.substring(0, image.length() - 1); + break; + default : + break; + } +} +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java new file mode 100644 index 000000000..5987f19d8 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java @@ -0,0 +1,35 @@ +/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java */ + +package org.apache.james.mime4j.field.address.parser; + +public interface AddressListParserTreeConstants +{ + public int JJTVOID = 0; + public int JJTADDRESS_LIST = 1; + public int JJTADDRESS = 2; + public int JJTMAILBOX = 3; + public int JJTNAME_ADDR = 4; + public int JJTGROUP_BODY = 5; + public int JJTANGLE_ADDR = 6; + public int JJTROUTE = 7; + public int JJTPHRASE = 8; + public int JJTADDR_SPEC = 9; + public int JJTLOCAL_PART = 10; + public int JJTDOMAIN = 11; + + + public String[] jjtNodeName = { + "void", + "address_list", + "address", + "mailbox", + "name_addr", + "group_body", + "angle_addr", + "route", + "phrase", + "addr_spec", + "local_part", + "domain", + }; +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java new file mode 100644 index 000000000..8ec2fe7d2 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java @@ -0,0 +1,19 @@ +/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java */ + +package org.apache.james.mime4j.field.address.parser; + +public interface AddressListParserVisitor +{ + public Object visit(SimpleNode node, Object data); + public Object visit(ASTaddress_list node, Object data); + public Object visit(ASTaddress node, Object data); + public Object visit(ASTmailbox node, Object data); + public Object visit(ASTname_addr node, Object data); + public Object visit(ASTgroup_body node, Object data); + public Object visit(ASTangle_addr node, Object data); + public Object visit(ASTroute node, Object data); + public Object visit(ASTphrase node, Object data); + public Object visit(ASTaddr_spec node, Object data); + public Object visit(ASTlocal_part node, Object data); + public Object visit(ASTdomain node, Object data); +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java new file mode 100644 index 000000000..780974616 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java @@ -0,0 +1,30 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field.address.parser; + +import org.apache.james.mime4j.field.address.parser.Node; +import org.apache.james.mime4j.field.address.parser.Token; + +public abstract class BaseNode implements Node { + + public Token firstToken; + public Token lastToken; + +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java new file mode 100644 index 000000000..08b5c5bef --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java @@ -0,0 +1,123 @@ +/* Generated By:JJTree: Do not edit this line. /Users/jason/Projects/apache-mime4j-0.3/target/generated-sources/jjtree/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java */ + +package org.apache.james.mime4j.field.address.parser; + +class JJTAddressListParserState { + private java.util.Stack<Node> nodes; + private java.util.Stack<Integer> marks; + + private int sp; // number of nodes on stack + private int mk; // current mark + private boolean node_created; + + JJTAddressListParserState() { + nodes = new java.util.Stack<Node>(); + marks = new java.util.Stack<Integer>(); + sp = 0; + mk = 0; + } + + /* Determines whether the current node was actually closed and + pushed. This should only be called in the final user action of a + node scope. */ + boolean nodeCreated() { + return node_created; + } + + /* Call this to reinitialize the node stack. It is called + automatically by the parser's ReInit() method. */ + void reset() { + nodes.removeAllElements(); + marks.removeAllElements(); + sp = 0; + mk = 0; + } + + /* Returns the root node of the AST. It only makes sense to call + this after a successful parse. */ + Node rootNode() { + return nodes.elementAt(0); + } + + /* Pushes a node on to the stack. */ + void pushNode(Node n) { + nodes.push(n); + ++sp; + } + + /* Returns the node on the top of the stack, and remove it from the + stack. */ + Node popNode() { + if (--sp < mk) { + mk = marks.pop().intValue(); + } + return nodes.pop(); + } + + /* Returns the node currently on the top of the stack. */ + Node peekNode() { + return nodes.peek(); + } + + /* Returns the number of children on the stack in the current node + scope. */ + int nodeArity() { + return sp - mk; + } + + + void clearNodeScope(Node n) { + while (sp > mk) { + popNode(); + } + mk = marks.pop().intValue(); + } + + + void openNodeScope(Node n) { + marks.push(new Integer(mk)); + mk = sp; + n.jjtOpen(); + } + + + /* A definite node is constructed from a specified number of + children. That number of nodes are popped from the stack and + made the children of the definite node. Then the definite node + is pushed on to the stack. */ + void closeNodeScope(Node n, int num) { + mk = marks.pop().intValue(); + while (num-- > 0) { + Node c = popNode(); + c.jjtSetParent(n); + n.jjtAddChild(c, num); + } + n.jjtClose(); + pushNode(n); + node_created = true; + } + + + /* A conditional node is constructed if its condition is true. All + the nodes that have been pushed since the node was opened are + made children of the the conditional node, which is then pushed + on to the stack. If the condition is false the node is not + constructed and they are left on the stack. */ + void closeNodeScope(Node n, boolean condition) { + if (condition) { + int a = nodeArity(); + mk = marks.pop().intValue(); + while (a-- > 0) { + Node c = popNode(); + c.jjtSetParent(n); + n.jjtAddChild(c, a); + } + n.jjtClose(); + pushNode(n); + node_created = true; + } else { + mk = marks.pop().intValue(); + node_created = false; + } + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java new file mode 100644 index 000000000..158892016 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java @@ -0,0 +1,37 @@ +/* Generated By:JJTree: Do not edit this line. Node.java */ + +package org.apache.james.mime4j.field.address.parser; + +/* All AST nodes must implement this interface. It provides basic + machinery for constructing the parent and child relationships + between nodes. */ + +public interface Node { + + /** This method is called after the node has been made the current + node. It indicates that child nodes can now be added to it. */ + public void jjtOpen(); + + /** This method is called after all the child nodes have been + added. */ + public void jjtClose(); + + /** This pair of methods are used to inform the node of its + parent. */ + public void jjtSetParent(Node n); + public Node jjtGetParent(); + + /** This method tells the node to add its argument to the node's + list of children. */ + public void jjtAddChild(Node n, int i); + + /** This method returns a child node. The children are numbered + from zero, left to right. */ + public Node jjtGetChild(int i); + + /** Return the number of children the node has. */ + public int jjtGetNumChildren(); + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data); +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java new file mode 100644 index 000000000..e20146fb6 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java @@ -0,0 +1,207 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.address.parser; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +public class ParseException extends Exception { + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. The boolean + * flag "specialConstructor" is also set to true to indicate that + * this constructor was used to create this object. + * This constructor calls its super class with the empty string + * to force the "toString" method of parent class "Throwable" to + * print the error message in the form: + * ParseException: <result of getMessage> + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(""); + specialConstructor = true; + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + specialConstructor = false; + } + + public ParseException(String message) { + super(message); + specialConstructor = false; + } + + /** + * This variable determines which constructor was used to create + * this object and thereby affects the semantics of the + * "getMessage" method (see below). + */ + protected boolean specialConstructor; + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * This method has the standard behavior when this object has been + * created using the standard constructors. Otherwise, it uses + * "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser), then this method is called during the printing + * of the final stack trace, and hence the correct error message + * gets displayed. + */ + public String getMessage() { + if (!specialConstructor) { + return super.getMessage(); + } + StringBuffer expected = new StringBuffer(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" "); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(eol).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) retval += " "; + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += add_escapes(tok.image); + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected.toString(); + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + protected String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java new file mode 100644 index 000000000..c9ba0b444 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java @@ -0,0 +1,454 @@ +/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.address.parser; + +/** + * An implementation of interface CharStream, where the stream is assumed to + * contain only ASCII characters (without unicode processing). + */ + +public class SimpleCharStream +{ + public static final boolean staticFlag = false; + int bufsize; + int available; + int tokenBegin; + public int bufpos = -1; + protected int bufline[]; + protected int bufcolumn[]; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int inBuf = 0; + protected int tabSize = 8; + + protected void setTabSize(int i) { tabSize = i; } + protected int getTabSize(int i) { return tabSize; } + + + protected void ExpandBuff(boolean wrapAround) + { + char[] newbuffer = new char[bufsize + 2048]; + int newbufline[] = new int[bufsize + 2048]; + int newbufcolumn[] = new int[bufsize + 2048]; + + try + { + if (wrapAround) + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, + bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos += (bufsize - tokenBegin)); + } + else + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos -= tokenBegin); + } + } + catch (Throwable t) + { + throw new Error(t.getMessage()); + } + + + bufsize += 2048; + available = bufsize; + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException + { + if (maxNextCharInd == available) + { + if (available == bufsize) + { + if (tokenBegin > 2048) + { + bufpos = maxNextCharInd = 0; + available = tokenBegin; + } + else if (tokenBegin < 0) + bufpos = maxNextCharInd = 0; + else + ExpandBuff(false); + } + else if (available > tokenBegin) + available = bufsize; + else if ((tokenBegin - available) < 2048) + ExpandBuff(true); + else + available = tokenBegin; + } + + int i; + try { + if ((i = inputStream.read(buffer, maxNextCharInd, + available - maxNextCharInd)) == -1) + { + inputStream.close(); + throw new java.io.IOException(); + } + else + maxNextCharInd += i; + return; + } + catch(java.io.IOException e) { + --bufpos; + backup(0); + if (tokenBegin == -1) + tokenBegin = bufpos; + throw e; + } + } + + public char BeginToken() throws java.io.IOException + { + tokenBegin = -1; + char c = readChar(); + tokenBegin = bufpos; + + return c; + } + + protected void UpdateLineColumn(char c) + { + column++; + + if (prevCharIsLF) + { + prevCharIsLF = false; + line += (column = 1); + } + else if (prevCharIsCR) + { + prevCharIsCR = false; + if (c == '\n') + { + prevCharIsLF = true; + } + else + line += (column = 1); + } + + switch (c) + { + case '\r' : + prevCharIsCR = true; + break; + case '\n' : + prevCharIsLF = true; + break; + case '\t' : + column--; + column += (tabSize - (column % tabSize)); + break; + default : + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + + public char readChar() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) + bufpos = 0; + + return buffer[bufpos]; + } + + if (++bufpos >= maxNextCharInd) + FillBuff(); + + char c = buffer[bufpos]; + + UpdateLineColumn(c); + return (c); + } + + /** + * @deprecated + * @see #getEndColumn + */ + @Deprecated + public int getColumn() { + return bufcolumn[bufpos]; + } + + /** + * @deprecated + * @see #getEndLine + */ + @Deprecated + public int getLine() { + return bufline[bufpos]; + } + + public int getEndColumn() { + return bufcolumn[bufpos]; + } + + public int getEndLine() { + return bufline[bufpos]; + } + + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + + public int getBeginLine() { + return bufline[tokenBegin]; + } + + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) + bufpos += bufsize; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.Reader dstream) + { + this(dstream, 1, 1, 4096); + } + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) + { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + bufpos = -1; + } + + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + + public void ReInit(java.io.Reader dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, 1, 1, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream) + { + this(dstream, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + public String GetImage() + { + if (bufpos >= tokenBegin) + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + else + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + + public char[] GetSuffix(int len) + { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + else + { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + public void Done() + { + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) + { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) + { + len = bufpos - tokenBegin + inBuf + 1; + } + else + { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && + bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) + { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) + { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) + { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) + bufline[j] = newLine++; + else + bufline[j] = newLine; + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java new file mode 100644 index 000000000..9bf537e60 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java @@ -0,0 +1,87 @@ +/* Generated By:JJTree: Do not edit this line. SimpleNode.java */ + +package org.apache.james.mime4j.field.address.parser; + +public class SimpleNode extends org.apache.james.mime4j.field.address.parser.BaseNode implements Node { + protected Node parent; + protected Node[] children; + protected int id; + protected AddressListParser parser; + + public SimpleNode(int i) { + id = i; + } + + public SimpleNode(AddressListParser p, int i) { + this(i); + parser = p; + } + + public void jjtOpen() { + } + + public void jjtClose() { + } + + public void jjtSetParent(Node n) { parent = n; } + public Node jjtGetParent() { return parent; } + + public void jjtAddChild(Node n, int i) { + if (children == null) { + children = new Node[i + 1]; + } else if (i >= children.length) { + Node c[] = new Node[i + 1]; + System.arraycopy(children, 0, c, 0, children.length); + children = c; + } + children[i] = n; + } + + public Node jjtGetChild(int i) { + return children[i]; + } + + public int jjtGetNumChildren() { + return (children == null) ? 0 : children.length; + } + + /** Accept the visitor. **/ + public Object jjtAccept(AddressListParserVisitor visitor, Object data) { + return visitor.visit(this, data); + } + + /** Accept the visitor. **/ + public Object childrenAccept(AddressListParserVisitor visitor, Object data) { + if (children != null) { + for (int i = 0; i < children.length; ++i) { + children[i].jjtAccept(visitor, data); + } + } + return data; + } + + /* You can override these two methods in subclasses of SimpleNode to + customize the way the node appears when the tree is dumped. If + your output uses more than one line you should override + toString(String), otherwise overriding toString() is probably all + you need to do. */ + + public String toString() { return AddressListParserTreeConstants.jjtNodeName[id]; } + public String toString(String prefix) { return prefix + toString(); } + + /* Override this method if you want to customize how the node dumps + out its children. */ + + public void dump(String prefix) { + System.out.println(toString(prefix)); + if (children != null) { + for (int i = 0; i < children.length; ++i) { + SimpleNode n = (SimpleNode)children[i]; + if (n != null) { + n.dump(prefix + " "); + } + } + } + } +} + diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java new file mode 100644 index 000000000..2382e8e92 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java @@ -0,0 +1,96 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.address.parser; + +/** + * Describes the input token stream. + */ + +public class Token { + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** + * beginLine and beginColumn describe the position of the first character + * of this token; endLine and endColumn describe the position of the + * last character of this token. + */ + public int beginLine, beginColumn, endLine, endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simlpy add something like : + * + * case MyParserConstants.ID : return new IDToken(); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use it in your lexical actions. + */ + public static final Token newToken(int ofKind) + { + switch(ofKind) + { + default : return new Token(); + } + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java new file mode 100644 index 000000000..0299c8523 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java @@ -0,0 +1,148 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.address.parser; + +public class TokenMgrError extends Error +{ + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occured. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt wass made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their espaced (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexicl error + * curLexState : lexical state in which this error occured + * errorLine : line number when the error occured + * errorColumn : column number when the error occured + * errorAfter : prefix that was seen before this error occured + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + public TokenMgrError() { + } + + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { + this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java new file mode 100644 index 000000000..cacf3af21 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java @@ -0,0 +1,268 @@ +/* Generated By:JavaCC: Do not edit this line. ContentTypeParser.java */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.contenttype.parser; + +import java.util.ArrayList; +import java.util.Vector; + +public class ContentTypeParser implements ContentTypeParserConstants { + + private String type; + private String subtype; + private ArrayList<String> paramNames = new ArrayList<String>(); + private ArrayList<String> paramValues = new ArrayList<String>(); + + public String getType() { return type; } + public String getSubType() { return subtype; } + public ArrayList<String> getParamNames() { return paramNames; } + public ArrayList<String> getParamValues() { return paramValues; } + + public static void main(String args[]) throws ParseException { + while (true) { + try { + ContentTypeParser parser = new ContentTypeParser(System.in); + parser.parseLine(); + } catch (Exception x) { + x.printStackTrace(); + return; + } + } + } + + final public void parseLine() throws ParseException { + parse(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 1: + jj_consume_token(1); + break; + default: + jj_la1[0] = jj_gen; + ; + } + jj_consume_token(2); + } + + final public void parseAll() throws ParseException { + parse(); + jj_consume_token(0); + } + + final public void parse() throws ParseException { + Token type; + Token subtype; + type = jj_consume_token(ATOKEN); + jj_consume_token(3); + subtype = jj_consume_token(ATOKEN); + this.type = type.image; + this.subtype = subtype.image; + label_1: + while (true) { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 4: + ; + break; + default: + jj_la1[1] = jj_gen; + break label_1; + } + jj_consume_token(4); + parameter(); + } + } + + final public void parameter() throws ParseException { + Token attrib; + String val; + attrib = jj_consume_token(ATOKEN); + jj_consume_token(5); + val = value(); + paramNames.add(attrib.image); + paramValues.add(val); + } + + final public String value() throws ParseException { + Token t; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case ATOKEN: + t = jj_consume_token(ATOKEN); + break; + case QUOTEDSTRING: + t = jj_consume_token(QUOTEDSTRING); + break; + default: + jj_la1[2] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + {if (true) return t.image;} + throw new Error("Missing return statement in function"); + } + + public ContentTypeParserTokenManager token_source; + SimpleCharStream jj_input_stream; + public Token token, jj_nt; + private int jj_ntk; + private int jj_gen; + final private int[] jj_la1 = new int[3]; + static private int[] jj_la1_0; + static { + jj_la1_0(); + } + private static void jj_la1_0() { + jj_la1_0 = new int[] {0x2,0x10,0x280000,}; + } + + public ContentTypeParser(java.io.InputStream stream) { + this(stream, null); + } + public ContentTypeParser(java.io.InputStream stream, String encoding) { + try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source = new ContentTypeParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + public void ReInit(java.io.InputStream stream, String encoding) { + try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + public ContentTypeParser(java.io.Reader stream) { + jj_input_stream = new SimpleCharStream(stream, 1, 1); + token_source = new ContentTypeParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.Reader stream) { + jj_input_stream.ReInit(stream, 1, 1); + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + public ContentTypeParser(ContentTypeParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + public void ReInit(ContentTypeParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 3; i++) jj_la1[i] = -1; + } + + final private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + final public Token getNextToken() { + if (token.next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + jj_gen++; + return token; + } + + final public Token getToken(int index) { + Token t = token; + for (int i = 0; i < index; i++) { + if (t.next != null) t = t.next; + else t = t.next = token_source.getNextToken(); + } + return t; + } + + final private int jj_ntk() { + if ((jj_nt=token.next) == null) + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + else + return (jj_ntk = jj_nt.kind); + } + + private Vector<int[]> jj_expentries = new Vector<int[]>(); + private int[] jj_expentry; + private int jj_kind = -1; + + public ParseException generateParseException() { + jj_expentries.removeAllElements(); + boolean[] la1tokens = new boolean[24]; + for (int i = 0; i < 24; i++) { + la1tokens[i] = false; + } + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 3; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1<<j)) != 0) { + la1tokens[j] = true; + } + } + } + } + for (int i = 0; i < 24; i++) { + if (la1tokens[i]) { + jj_expentry = new int[1]; + jj_expentry[0] = i; + jj_expentries.addElement(jj_expentry); + } + } + int[][] exptokseq = new int[jj_expentries.size()][]; + for (int i = 0; i < jj_expentries.size(); i++) { + exptokseq[i] = jj_expentries.elementAt(i); + } + return new ParseException(token, exptokseq, tokenImage); + } + + final public void enable_tracing() { + } + + final public void disable_tracing() { + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java new file mode 100644 index 000000000..d933d800d --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java @@ -0,0 +1,62 @@ +/* Generated By:JavaCC: Do not edit this line. ContentTypeParserConstants.java */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.contenttype.parser; + +public interface ContentTypeParserConstants { + + int EOF = 0; + int WS = 6; + int COMMENT = 8; + int QUOTEDSTRING = 19; + int DIGITS = 20; + int ATOKEN = 21; + int QUOTEDPAIR = 22; + int ANY = 23; + + int DEFAULT = 0; + int INCOMMENT = 1; + int NESTED_COMMENT = 2; + int INQUOTEDSTRING = 3; + + String[] tokenImage = { + "<EOF>", + "\"\\r\"", + "\"\\n\"", + "\"/\"", + "\";\"", + "\"=\"", + "<WS>", + "\"(\"", + "\")\"", + "<token of kind 9>", + "\"(\"", + "<token of kind 11>", + "<token of kind 12>", + "\"(\"", + "\")\"", + "<token of kind 15>", + "\"\\\"\"", + "<token of kind 17>", + "<token of kind 18>", + "\"\\\"\"", + "<DIGITS>", + "<ATOKEN>", + "<QUOTEDPAIR>", + "<ANY>", + }; + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java new file mode 100644 index 000000000..25b7abafa --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java @@ -0,0 +1,877 @@ +/* Generated By:JavaCC: Do not edit this line. ContentTypeParserTokenManager.java */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.contenttype.parser; +import java.util.ArrayList; + +public class ContentTypeParserTokenManager implements ContentTypeParserConstants +{ + // Keeps track of how many levels of comment nesting + // we've encountered. This is only used when the 2nd + // level is reached, for example ((this)), not (this). + // This is because the outermost level must be treated + // specially anyway, because the outermost ")" has a + // different token type than inner ")" instances. + static int commentNest; + public java.io.PrintStream debugStream = System.out; + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_0(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_0(int pos, long active0) +{ + return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1); +} +private final int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private final int jjStartNfaWithStates_0(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_0(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_0() +{ + switch(curChar) + { + case 10: + return jjStartNfaWithStates_0(0, 2, 2); + case 13: + return jjStartNfaWithStates_0(0, 1, 2); + case 34: + return jjStopAtPos(0, 16); + case 40: + return jjStopAtPos(0, 7); + case 47: + return jjStopAtPos(0, 3); + case 59: + return jjStopAtPos(0, 4); + case 61: + return jjStopAtPos(0, 5); + default : + return jjMoveNfa_0(3, 0); + } +} +private final void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private final void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private final void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} +private final void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} +private final void jjCheckNAddStates(int start) +{ + jjCheckNAdd(jjnextStates[start]); + jjCheckNAdd(jjnextStates[start + 1]); +} +static final long[] jjbitVec0 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private final int jjMoveNfa_0(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 3: + if ((0x3ff6cfafffffdffL & l) != 0L) + { + if (kind > 21) + kind = 21; + jjCheckNAdd(2); + } + else if ((0x100000200L & l) != 0L) + { + if (kind > 6) + kind = 6; + jjCheckNAdd(0); + } + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 20) + kind = 20; + jjCheckNAdd(1); + } + break; + case 0: + if ((0x100000200L & l) == 0L) + break; + kind = 6; + jjCheckNAdd(0); + break; + case 1: + if ((0x3ff000000000000L & l) == 0L) + break; + if (kind > 20) + kind = 20; + jjCheckNAdd(1); + break; + case 2: + if ((0x3ff6cfafffffdffL & l) == 0L) + break; + if (kind > 21) + kind = 21; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 3: + case 2: + if ((0xffffffffc7fffffeL & l) == 0L) + break; + kind = 21; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 3: + case 2: + if ((jjbitVec0[i2] & l2) == 0L) + break; + if (kind > 21) + kind = 21; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_1(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_1(int pos, long active0) +{ + return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_1(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_1(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_1() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 10); + case 41: + return jjStopAtPos(0, 8); + default : + return jjMoveNfa_1(0, 0); + } +} +private final int jjMoveNfa_1(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 11) + kind = 11; + break; + case 1: + if (kind > 9) + kind = 9; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 11) + kind = 11; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 9) + kind = 9; + break; + case 2: + if (kind > 11) + kind = 11; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 11) + kind = 11; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 9) + kind = 9; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_3(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_3(int pos, long active0) +{ + return jjMoveNfa_3(jjStopStringLiteralDfa_3(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_3(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_3(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_3() +{ + switch(curChar) + { + case 34: + return jjStopAtPos(0, 19); + default : + return jjMoveNfa_3(0, 0); + } +} +private final int jjMoveNfa_3(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 2: + if ((0xfffffffbffffffffL & l) == 0L) + break; + if (kind > 18) + kind = 18; + jjCheckNAdd(2); + break; + case 1: + if (kind > 17) + kind = 17; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0xffffffffefffffffL & l) != 0L) + { + if (kind > 18) + kind = 18; + jjCheckNAdd(2); + } + else if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 17) + kind = 17; + break; + case 2: + if ((0xffffffffefffffffL & l) == 0L) + break; + if (kind > 18) + kind = 18; + jjCheckNAdd(2); + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + case 2: + if ((jjbitVec0[i2] & l2) == 0L) + break; + if (kind > 18) + kind = 18; + jjCheckNAdd(2); + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 17) + kind = 17; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_2(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_2(int pos, long active0) +{ + return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_2(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_2(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_2() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 13); + case 41: + return jjStopAtPos(0, 14); + default : + return jjMoveNfa_2(0, 0); + } +} +private final int jjMoveNfa_2(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 15) + kind = 15; + break; + case 1: + if (kind > 12) + kind = 12; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 15) + kind = 15; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 12) + kind = 12; + break; + case 2: + if (kind > 15) + kind = 15; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 15) + kind = 15; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 12) + kind = 12; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +static final int[] jjnextStates = { +}; +public static final String[] jjstrLiteralImages = { +"", "\15", "\12", "\57", "\73", "\75", null, null, null, null, null, null, +null, null, null, null, null, null, null, null, null, null, null, null, }; +public static final String[] lexStateNames = { + "DEFAULT", + "INCOMMENT", + "NESTED_COMMENT", + "INQUOTEDSTRING", +}; +public static final int[] jjnewLexState = { + -1, -1, -1, -1, -1, -1, -1, 1, 0, -1, 2, -1, -1, -1, -1, -1, 3, -1, -1, 0, -1, -1, -1, -1, +}; +static final long[] jjtoToken = { + 0x38003fL, +}; +static final long[] jjtoSkip = { + 0x140L, +}; +static final long[] jjtoSpecial = { + 0x40L, +}; +static final long[] jjtoMore = { + 0x7fe80L, +}; +protected SimpleCharStream input_stream; +private final int[] jjrounds = new int[3]; +private final int[] jjstateSet = new int[6]; +StringBuffer image; +int jjimageLen; +int lengthOfMatch; +protected char curChar; +public ContentTypeParserTokenManager(SimpleCharStream stream){ + if (SimpleCharStream.staticFlag) + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + input_stream = stream; +} +public ContentTypeParserTokenManager(SimpleCharStream stream, int lexState){ + this(stream); + SwitchTo(lexState); +} +public void ReInit(SimpleCharStream stream) +{ + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); +} +private final void ReInitRounds() +{ + int i; + jjround = 0x80000001; + for (i = 3; i-- > 0;) + jjrounds[i] = 0x80000000; +} +public void ReInit(SimpleCharStream stream, int lexState) +{ + ReInit(stream); + SwitchTo(lexState); +} +public void SwitchTo(int lexState) +{ + if (lexState >= 4 || lexState < 0) + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + else + curLexState = lexState; +} + +protected Token jjFillToken() +{ + Token t = Token.newToken(jjmatchedKind); + t.kind = jjmatchedKind; + String im = jjstrLiteralImages[jjmatchedKind]; + t.image = (im == null) ? input_stream.GetImage() : im; + t.beginLine = input_stream.getBeginLine(); + t.beginColumn = input_stream.getBeginColumn(); + t.endLine = input_stream.getEndLine(); + t.endColumn = input_stream.getEndColumn(); + return t; +} + +int curLexState = 0; +int defaultLexState = 0; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +public Token getNextToken() +{ + int kind; + Token specialToken = null; + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(java.io.IOException e) + { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + return matchedToken; + } + image = null; + jjimageLen = 0; + + for (;;) + { + switch(curLexState) + { + case 0: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + break; + case 1: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_1(); + break; + case 2: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_2(); + break; + case 3: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_3(); + break; + } + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) + input_stream.backup(curPos - jjmatchedPos - 1); + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + TokenLexicalActions(matchedToken); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + return matchedToken; + } + else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + if (specialToken == null) + specialToken = matchedToken; + else + { + matchedToken.specialToken = specialToken; + specialToken = (specialToken.next = matchedToken); + } + } + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + continue EOFLoop; + } + MoreLexicalActions(); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + curPos = 0; + jjmatchedKind = 0x7fffffff; + try { + curChar = input_stream.readChar(); + continue; + } + catch (java.io.IOException e1) { } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } + else + error_column++; + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } + } +} + +void MoreLexicalActions() +{ + jjimageLen += (lengthOfMatch = jjmatchedPos + 1); + switch(jjmatchedKind) + { + case 9 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 10 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + commentNest = 1; + break; + case 12 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 13 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + ++commentNest; + break; + case 14 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT); + break; + case 16 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 1); + break; + case 17 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + default : + break; + } +} +void TokenLexicalActions(Token matchedToken) +{ + switch(jjmatchedKind) + { + case 19 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen + (lengthOfMatch = jjmatchedPos + 1))); + matchedToken.image = image.substring(0, image.length() - 1); + break; + default : + break; + } +} +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java new file mode 100644 index 000000000..d9b69b25c --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java @@ -0,0 +1,207 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.contenttype.parser; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +public class ParseException extends Exception { + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. The boolean + * flag "specialConstructor" is also set to true to indicate that + * this constructor was used to create this object. + * This constructor calls its super class with the empty string + * to force the "toString" method of parent class "Throwable" to + * print the error message in the form: + * ParseException: <result of getMessage> + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(""); + specialConstructor = true; + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + specialConstructor = false; + } + + public ParseException(String message) { + super(message); + specialConstructor = false; + } + + /** + * This variable determines which constructor was used to create + * this object and thereby affects the semantics of the + * "getMessage" method (see below). + */ + protected boolean specialConstructor; + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * This method has the standard behavior when this object has been + * created using the standard constructors. Otherwise, it uses + * "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser), then this method is called during the printing + * of the final stack trace, and hence the correct error message + * gets displayed. + */ + public String getMessage() { + if (!specialConstructor) { + return super.getMessage(); + } + StringBuffer expected = new StringBuffer(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" "); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(eol).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) retval += " "; + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += add_escapes(tok.image); + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected.toString(); + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + protected String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java new file mode 100644 index 000000000..ae035b717 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java @@ -0,0 +1,454 @@ +/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.contenttype.parser; + +/** + * An implementation of interface CharStream, where the stream is assumed to + * contain only ASCII characters (without unicode processing). + */ + +public class SimpleCharStream +{ + public static final boolean staticFlag = false; + int bufsize; + int available; + int tokenBegin; + public int bufpos = -1; + protected int bufline[]; + protected int bufcolumn[]; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int inBuf = 0; + protected int tabSize = 8; + + protected void setTabSize(int i) { tabSize = i; } + protected int getTabSize(int i) { return tabSize; } + + + protected void ExpandBuff(boolean wrapAround) + { + char[] newbuffer = new char[bufsize + 2048]; + int newbufline[] = new int[bufsize + 2048]; + int newbufcolumn[] = new int[bufsize + 2048]; + + try + { + if (wrapAround) + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, + bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos += (bufsize - tokenBegin)); + } + else + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos -= tokenBegin); + } + } + catch (Throwable t) + { + throw new Error(t.getMessage()); + } + + + bufsize += 2048; + available = bufsize; + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException + { + if (maxNextCharInd == available) + { + if (available == bufsize) + { + if (tokenBegin > 2048) + { + bufpos = maxNextCharInd = 0; + available = tokenBegin; + } + else if (tokenBegin < 0) + bufpos = maxNextCharInd = 0; + else + ExpandBuff(false); + } + else if (available > tokenBegin) + available = bufsize; + else if ((tokenBegin - available) < 2048) + ExpandBuff(true); + else + available = tokenBegin; + } + + int i; + try { + if ((i = inputStream.read(buffer, maxNextCharInd, + available - maxNextCharInd)) == -1) + { + inputStream.close(); + throw new java.io.IOException(); + } + else + maxNextCharInd += i; + return; + } + catch(java.io.IOException e) { + --bufpos; + backup(0); + if (tokenBegin == -1) + tokenBegin = bufpos; + throw e; + } + } + + public char BeginToken() throws java.io.IOException + { + tokenBegin = -1; + char c = readChar(); + tokenBegin = bufpos; + + return c; + } + + protected void UpdateLineColumn(char c) + { + column++; + + if (prevCharIsLF) + { + prevCharIsLF = false; + line += (column = 1); + } + else if (prevCharIsCR) + { + prevCharIsCR = false; + if (c == '\n') + { + prevCharIsLF = true; + } + else + line += (column = 1); + } + + switch (c) + { + case '\r' : + prevCharIsCR = true; + break; + case '\n' : + prevCharIsLF = true; + break; + case '\t' : + column--; + column += (tabSize - (column % tabSize)); + break; + default : + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + + public char readChar() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) + bufpos = 0; + + return buffer[bufpos]; + } + + if (++bufpos >= maxNextCharInd) + FillBuff(); + + char c = buffer[bufpos]; + + UpdateLineColumn(c); + return (c); + } + + /** + * @deprecated + * @see #getEndColumn + */ + @Deprecated + public int getColumn() { + return bufcolumn[bufpos]; + } + + /** + * @deprecated + * @see #getEndLine + */ + @Deprecated + public int getLine() { + return bufline[bufpos]; + } + + public int getEndColumn() { + return bufcolumn[bufpos]; + } + + public int getEndLine() { + return bufline[bufpos]; + } + + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + + public int getBeginLine() { + return bufline[tokenBegin]; + } + + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) + bufpos += bufsize; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.Reader dstream) + { + this(dstream, 1, 1, 4096); + } + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) + { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + bufpos = -1; + } + + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + + public void ReInit(java.io.Reader dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, 1, 1, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream) + { + this(dstream, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + public String GetImage() + { + if (bufpos >= tokenBegin) + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + else + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + + public char[] GetSuffix(int len) + { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + else + { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + public void Done() + { + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) + { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) + { + len = bufpos - tokenBegin + inBuf + 1; + } + else + { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && + bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) + { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) + { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) + { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) + bufline[j] = newLine++; + else + bufline[j] = newLine; + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java new file mode 100644 index 000000000..34e65eec0 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java @@ -0,0 +1,96 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.contenttype.parser; + +/** + * Describes the input token stream. + */ + +public class Token { + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** + * beginLine and beginColumn describe the position of the first character + * of this token; endLine and endColumn describe the position of the + * last character of this token. + */ + public int beginLine, beginColumn, endLine, endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simlpy add something like : + * + * case MyParserConstants.ID : return new IDToken(); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use it in your lexical actions. + */ + public static final Token newToken(int ofKind) + { + switch(ofKind) + { + default : return new Token(); + } + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java new file mode 100644 index 000000000..ea5a7826e --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java @@ -0,0 +1,148 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.contenttype.parser; + +public class TokenMgrError extends Error +{ + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occured. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt wass made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their espaced (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexicl error + * curLexState : lexical state in which this error occured + * errorLine : line number when the error occured + * errorColumn : column number when the error occured + * errorAfter : prefix that was seen before this error occured + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + public TokenMgrError() { + } + + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { + this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java new file mode 100644 index 000000000..506ff54e5 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java @@ -0,0 +1,127 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.field.datetime; + +import org.apache.james.mime4j.field.datetime.parser.DateTimeParser; +import org.apache.james.mime4j.field.datetime.parser.ParseException; +import org.apache.james.mime4j.field.datetime.parser.TokenMgrError; + +import java.util.Date; +import java.util.Calendar; +import java.util.TimeZone; +import java.util.GregorianCalendar; +import java.io.StringReader; + +public class DateTime { + private final Date date; + private final int year; + private final int month; + private final int day; + private final int hour; + private final int minute; + private final int second; + private final int timeZone; + + public DateTime(String yearString, int month, int day, int hour, int minute, int second, int timeZone) { + this.year = convertToYear(yearString); + this.date = convertToDate(year, month, day, hour, minute, second, timeZone); + this.month = month; + this.day = day; + this.hour = hour; + this.minute = minute; + this.second = second; + this.timeZone = timeZone; + } + + private int convertToYear(String yearString) { + int year = Integer.parseInt(yearString); + switch (yearString.length()) { + case 1: + case 2: + if (year >= 0 && year < 50) + return 2000 + year; + else + return 1900 + year; + case 3: + return 1900 + year; + default: + return year; + } + } + + public static Date convertToDate(int year, int month, int day, int hour, int minute, int second, int timeZone) { + Calendar c = new GregorianCalendar(TimeZone.getTimeZone("GMT+0")); + c.set(year, month - 1, day, hour, minute, second); + c.set(Calendar.MILLISECOND, 0); + + if (timeZone != Integer.MIN_VALUE) { + int minutes = ((timeZone / 100) * 60) + timeZone % 100; + c.add(Calendar.MINUTE, -1 * minutes); + } + + return c.getTime(); + } + + public Date getDate() { + return date; + } + + public int getYear() { + return year; + } + + public int getMonth() { + return month; + } + + public int getDay() { + return day; + } + + public int getHour() { + return hour; + } + + public int getMinute() { + return minute; + } + + public int getSecond() { + return second; + } + + public int getTimeZone() { + return timeZone; + } + + public void print() { + System.out.println(getYear() + " " + getMonth() + " " + getDay() + "; " + getHour() + " " + getMinute() + " " + getSecond() + " " + getTimeZone()); + } + + + public static DateTime parse(String dateString) throws ParseException { + try { + return new DateTimeParser(new StringReader(dateString)).parseAll(); + } + catch (TokenMgrError err) { + throw new ParseException(err.getMessage()); + } + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java new file mode 100644 index 000000000..43edebb5c --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java @@ -0,0 +1,570 @@ +/* Generated By:JavaCC: Do not edit this line. DateTimeParser.java */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.datetime.parser; + +import org.apache.james.mime4j.field.datetime.DateTime; + +import java.util.Vector; + +public class DateTimeParser implements DateTimeParserConstants { + private static final boolean ignoreMilitaryZoneOffset = true; + + public static void main(String args[]) throws ParseException { + while (true) { + try { + DateTimeParser parser = new DateTimeParser(System.in); + parser.parseLine(); + } catch (Exception x) { + x.printStackTrace(); + return; + } + } + } + + private static int parseDigits(Token token) { + return Integer.parseInt(token.image, 10); + } + + private static int getMilitaryZoneOffset(char c) { + if (ignoreMilitaryZoneOffset) + return 0; + + c = Character.toUpperCase(c); + + switch (c) { + case 'A': return 1; + case 'B': return 2; + case 'C': return 3; + case 'D': return 4; + case 'E': return 5; + case 'F': return 6; + case 'G': return 7; + case 'H': return 8; + case 'I': return 9; + case 'K': return 10; + case 'L': return 11; + case 'M': return 12; + + case 'N': return -1; + case 'O': return -2; + case 'P': return -3; + case 'Q': return -4; + case 'R': return -5; + case 'S': return -6; + case 'T': return -7; + case 'U': return -8; + case 'V': return -9; + case 'W': return -10; + case 'X': return -11; + case 'Y': return -12; + + case 'Z': return 0; + default: return 0; + } + } + + private static class Time { + private int hour; + private int minute; + private int second; + private int zone; + + public Time(int hour, int minute, int second, int zone) { + this.hour = hour; + this.minute = minute; + this.second = second; + this.zone = zone; + } + + public int getHour() { return hour; } + public int getMinute() { return minute; } + public int getSecond() { return second; } + public int getZone() { return zone; } + } + + private static class Date { + private String year; + private int month; + private int day; + + public Date(String year, int month, int day) { + this.year = year; + this.month = month; + this.day = day; + } + + public String getYear() { return year; } + public int getMonth() { return month; } + public int getDay() { return day; } + } + + final public DateTime parseLine() throws ParseException { + DateTime dt; + dt = date_time(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 1: + jj_consume_token(1); + break; + default: + jj_la1[0] = jj_gen; + ; + } + jj_consume_token(2); + {if (true) return dt;} + throw new Error("Missing return statement in function"); + } + + final public DateTime parseAll() throws ParseException { + DateTime dt; + dt = date_time(); + jj_consume_token(0); + {if (true) return dt;} + throw new Error("Missing return statement in function"); + } + + final public DateTime date_time() throws ParseException { + Date d; Time t; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 4: + case 5: + case 6: + case 7: + case 8: + case 9: + case 10: + day_of_week(); + jj_consume_token(3); + break; + default: + jj_la1[1] = jj_gen; + ; + } + d = date(); + t = time(); + {if (true) return new DateTime( + d.getYear(), + d.getMonth(), + d.getDay(), + t.getHour(), + t.getMinute(), + t.getSecond(), + t.getZone());} // time zone offset + + throw new Error("Missing return statement in function"); + } + + final public String day_of_week() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 4: + jj_consume_token(4); + break; + case 5: + jj_consume_token(5); + break; + case 6: + jj_consume_token(6); + break; + case 7: + jj_consume_token(7); + break; + case 8: + jj_consume_token(8); + break; + case 9: + jj_consume_token(9); + break; + case 10: + jj_consume_token(10); + break; + default: + jj_la1[2] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + {if (true) return token.image;} + throw new Error("Missing return statement in function"); + } + + final public Date date() throws ParseException { + int d, m; String y; + d = day(); + m = month(); + y = year(); + {if (true) return new Date(y, m, d);} + throw new Error("Missing return statement in function"); + } + + final public int day() throws ParseException { + Token t; + t = jj_consume_token(DIGITS); + {if (true) return parseDigits(t);} + throw new Error("Missing return statement in function"); + } + + final public int month() throws ParseException { + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 11: + jj_consume_token(11); + {if (true) return 1;} + break; + case 12: + jj_consume_token(12); + {if (true) return 2;} + break; + case 13: + jj_consume_token(13); + {if (true) return 3;} + break; + case 14: + jj_consume_token(14); + {if (true) return 4;} + break; + case 15: + jj_consume_token(15); + {if (true) return 5;} + break; + case 16: + jj_consume_token(16); + {if (true) return 6;} + break; + case 17: + jj_consume_token(17); + {if (true) return 7;} + break; + case 18: + jj_consume_token(18); + {if (true) return 8;} + break; + case 19: + jj_consume_token(19); + {if (true) return 9;} + break; + case 20: + jj_consume_token(20); + {if (true) return 10;} + break; + case 21: + jj_consume_token(21); + {if (true) return 11;} + break; + case 22: + jj_consume_token(22); + {if (true) return 12;} + break; + default: + jj_la1[3] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + throw new Error("Missing return statement in function"); + } + + final public String year() throws ParseException { + Token t; + t = jj_consume_token(DIGITS); + {if (true) return t.image;} + throw new Error("Missing return statement in function"); + } + + final public Time time() throws ParseException { + int h, m, s=0, z; + h = hour(); + jj_consume_token(23); + m = minute(); + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 23: + jj_consume_token(23); + s = second(); + break; + default: + jj_la1[4] = jj_gen; + ; + } + z = zone(); + {if (true) return new Time(h, m, s, z);} + throw new Error("Missing return statement in function"); + } + + final public int hour() throws ParseException { + Token t; + t = jj_consume_token(DIGITS); + {if (true) return parseDigits(t);} + throw new Error("Missing return statement in function"); + } + + final public int minute() throws ParseException { + Token t; + t = jj_consume_token(DIGITS); + {if (true) return parseDigits(t);} + throw new Error("Missing return statement in function"); + } + + final public int second() throws ParseException { + Token t; + t = jj_consume_token(DIGITS); + {if (true) return parseDigits(t);} + throw new Error("Missing return statement in function"); + } + + final public int zone() throws ParseException { + Token t, u; int z; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case OFFSETDIR: + t = jj_consume_token(OFFSETDIR); + u = jj_consume_token(DIGITS); + z=parseDigits(u)*(t.image.equals("-") ? -1 : 1); + break; + case 25: + case 26: + case 27: + case 28: + case 29: + case 30: + case 31: + case 32: + case 33: + case 34: + case MILITARY_ZONE: + z = obs_zone(); + break; + default: + jj_la1[5] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + {if (true) return z;} + throw new Error("Missing return statement in function"); + } + + final public int obs_zone() throws ParseException { + Token t; int z; + switch ((jj_ntk==-1)?jj_ntk():jj_ntk) { + case 25: + jj_consume_token(25); + z=0; + break; + case 26: + jj_consume_token(26); + z=0; + break; + case 27: + jj_consume_token(27); + z=-5; + break; + case 28: + jj_consume_token(28); + z=-4; + break; + case 29: + jj_consume_token(29); + z=-6; + break; + case 30: + jj_consume_token(30); + z=-5; + break; + case 31: + jj_consume_token(31); + z=-7; + break; + case 32: + jj_consume_token(32); + z=-6; + break; + case 33: + jj_consume_token(33); + z=-8; + break; + case 34: + jj_consume_token(34); + z=-7; + break; + case MILITARY_ZONE: + t = jj_consume_token(MILITARY_ZONE); + z=getMilitaryZoneOffset(t.image.charAt(0)); + break; + default: + jj_la1[6] = jj_gen; + jj_consume_token(-1); + throw new ParseException(); + } + {if (true) return z * 100;} + throw new Error("Missing return statement in function"); + } + + public DateTimeParserTokenManager token_source; + SimpleCharStream jj_input_stream; + public Token token, jj_nt; + private int jj_ntk; + private int jj_gen; + final private int[] jj_la1 = new int[7]; + static private int[] jj_la1_0; + static private int[] jj_la1_1; + static { + jj_la1_0(); + jj_la1_1(); + } + private static void jj_la1_0() { + jj_la1_0 = new int[] {0x2,0x7f0,0x7f0,0x7ff800,0x800000,0xff000000,0xfe000000,}; + } + private static void jj_la1_1() { + jj_la1_1 = new int[] {0x0,0x0,0x0,0x0,0x0,0xf,0xf,}; + } + + public DateTimeParser(java.io.InputStream stream) { + this(stream, null); + } + public DateTimeParser(java.io.InputStream stream, String encoding) { + try { jj_input_stream = new SimpleCharStream(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source = new DateTimeParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.InputStream stream) { + ReInit(stream, null); + } + public void ReInit(java.io.InputStream stream, String encoding) { + try { jj_input_stream.ReInit(stream, encoding, 1, 1); } catch(java.io.UnsupportedEncodingException e) { throw new RuntimeException(e); } + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + public DateTimeParser(java.io.Reader stream) { + jj_input_stream = new SimpleCharStream(stream, 1, 1); + token_source = new DateTimeParserTokenManager(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + public void ReInit(java.io.Reader stream) { + jj_input_stream.ReInit(stream, 1, 1); + token_source.ReInit(jj_input_stream); + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + public DateTimeParser(DateTimeParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + public void ReInit(DateTimeParserTokenManager tm) { + token_source = tm; + token = new Token(); + jj_ntk = -1; + jj_gen = 0; + for (int i = 0; i < 7; i++) jj_la1[i] = -1; + } + + final private Token jj_consume_token(int kind) throws ParseException { + Token oldToken; + if ((oldToken = token).next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + if (token.kind == kind) { + jj_gen++; + return token; + } + token = oldToken; + jj_kind = kind; + throw generateParseException(); + } + + final public Token getNextToken() { + if (token.next != null) token = token.next; + else token = token.next = token_source.getNextToken(); + jj_ntk = -1; + jj_gen++; + return token; + } + + final public Token getToken(int index) { + Token t = token; + for (int i = 0; i < index; i++) { + if (t.next != null) t = t.next; + else t = t.next = token_source.getNextToken(); + } + return t; + } + + final private int jj_ntk() { + if ((jj_nt=token.next) == null) + return (jj_ntk = (token.next=token_source.getNextToken()).kind); + else + return (jj_ntk = jj_nt.kind); + } + + private Vector<int[]> jj_expentries = new Vector<int[]>(); + private int[] jj_expentry; + private int jj_kind = -1; + + public ParseException generateParseException() { + jj_expentries.removeAllElements(); + boolean[] la1tokens = new boolean[49]; + for (int i = 0; i < 49; i++) { + la1tokens[i] = false; + } + if (jj_kind >= 0) { + la1tokens[jj_kind] = true; + jj_kind = -1; + } + for (int i = 0; i < 7; i++) { + if (jj_la1[i] == jj_gen) { + for (int j = 0; j < 32; j++) { + if ((jj_la1_0[i] & (1<<j)) != 0) { + la1tokens[j] = true; + } + if ((jj_la1_1[i] & (1<<j)) != 0) { + la1tokens[32+j] = true; + } + } + } + } + for (int i = 0; i < 49; i++) { + if (la1tokens[i]) { + jj_expentry = new int[1]; + jj_expentry[0] = i; + jj_expentries.addElement(jj_expentry); + } + } + int[][] exptokseq = new int[jj_expentries.size()][]; + for (int i = 0; i < jj_expentries.size(); i++) { + exptokseq[i] = jj_expentries.elementAt(i); + } + return new ParseException(token, exptokseq, tokenImage); + } + + final public void enable_tracing() { + } + + final public void disable_tracing() { + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java new file mode 100644 index 000000000..2c203db2e --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java @@ -0,0 +1,86 @@ +/* Generated By:JavaCC: Do not edit this line. DateTimeParserConstants.java */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.datetime.parser; + +public interface DateTimeParserConstants { + + int EOF = 0; + int OFFSETDIR = 24; + int MILITARY_ZONE = 35; + int WS = 36; + int COMMENT = 38; + int DIGITS = 46; + int QUOTEDPAIR = 47; + int ANY = 48; + + int DEFAULT = 0; + int INCOMMENT = 1; + int NESTED_COMMENT = 2; + + String[] tokenImage = { + "<EOF>", + "\"\\r\"", + "\"\\n\"", + "\",\"", + "\"Mon\"", + "\"Tue\"", + "\"Wed\"", + "\"Thu\"", + "\"Fri\"", + "\"Sat\"", + "\"Sun\"", + "\"Jan\"", + "\"Feb\"", + "\"Mar\"", + "\"Apr\"", + "\"May\"", + "\"Jun\"", + "\"Jul\"", + "\"Aug\"", + "\"Sep\"", + "\"Oct\"", + "\"Nov\"", + "\"Dec\"", + "\":\"", + "<OFFSETDIR>", + "\"UT\"", + "\"GMT\"", + "\"EST\"", + "\"EDT\"", + "\"CST\"", + "\"CDT\"", + "\"MST\"", + "\"MDT\"", + "\"PST\"", + "\"PDT\"", + "<MILITARY_ZONE>", + "<WS>", + "\"(\"", + "\")\"", + "<token of kind 39>", + "\"(\"", + "<token of kind 41>", + "<token of kind 42>", + "\"(\"", + "\")\"", + "<token of kind 45>", + "<DIGITS>", + "<QUOTEDPAIR>", + "<ANY>", + }; + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java new file mode 100644 index 000000000..4b2d2fd95 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java @@ -0,0 +1,882 @@ +/* Generated By:JavaCC: Do not edit this line. DateTimeParserTokenManager.java */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.datetime.parser; +import org.apache.james.mime4j.field.datetime.DateTime; +import java.util.Calendar; + +public class DateTimeParserTokenManager implements DateTimeParserConstants +{ + // Keeps track of how many levels of comment nesting + // we've encountered. This is only used when the 2nd + // level is reached, for example ((this)), not (this). + // This is because the outermost level must be treated + // specially anyway, because the outermost ")" has a + // different token type than inner ")" instances. + static int commentNest; + public java.io.PrintStream debugStream = System.out; + public void setDebugStream(java.io.PrintStream ds) { debugStream = ds; } +private final int jjStopStringLiteralDfa_0(int pos, long active0) +{ + switch (pos) + { + case 0: + if ((active0 & 0x7fe7cf7f0L) != 0L) + { + jjmatchedKind = 35; + return -1; + } + return -1; + case 1: + if ((active0 & 0x7fe7cf7f0L) != 0L) + { + if (jjmatchedPos == 0) + { + jjmatchedKind = 35; + jjmatchedPos = 0; + } + return -1; + } + return -1; + default : + return -1; + } +} +private final int jjStartNfa_0(int pos, long active0) +{ + return jjMoveNfa_0(jjStopStringLiteralDfa_0(pos, active0), pos + 1); +} +private final int jjStopAtPos(int pos, int kind) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + return pos + 1; +} +private final int jjStartNfaWithStates_0(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_0(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_0() +{ + switch(curChar) + { + case 10: + return jjStopAtPos(0, 2); + case 13: + return jjStopAtPos(0, 1); + case 40: + return jjStopAtPos(0, 37); + case 44: + return jjStopAtPos(0, 3); + case 58: + return jjStopAtPos(0, 23); + case 65: + return jjMoveStringLiteralDfa1_0(0x44000L); + case 67: + return jjMoveStringLiteralDfa1_0(0x60000000L); + case 68: + return jjMoveStringLiteralDfa1_0(0x400000L); + case 69: + return jjMoveStringLiteralDfa1_0(0x18000000L); + case 70: + return jjMoveStringLiteralDfa1_0(0x1100L); + case 71: + return jjMoveStringLiteralDfa1_0(0x4000000L); + case 74: + return jjMoveStringLiteralDfa1_0(0x30800L); + case 77: + return jjMoveStringLiteralDfa1_0(0x18000a010L); + case 78: + return jjMoveStringLiteralDfa1_0(0x200000L); + case 79: + return jjMoveStringLiteralDfa1_0(0x100000L); + case 80: + return jjMoveStringLiteralDfa1_0(0x600000000L); + case 83: + return jjMoveStringLiteralDfa1_0(0x80600L); + case 84: + return jjMoveStringLiteralDfa1_0(0xa0L); + case 85: + return jjMoveStringLiteralDfa1_0(0x2000000L); + case 87: + return jjMoveStringLiteralDfa1_0(0x40L); + default : + return jjMoveNfa_0(0, 0); + } +} +private final int jjMoveStringLiteralDfa1_0(long active0) +{ + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(0, active0); + return 1; + } + switch(curChar) + { + case 68: + return jjMoveStringLiteralDfa2_0(active0, 0x550000000L); + case 77: + return jjMoveStringLiteralDfa2_0(active0, 0x4000000L); + case 83: + return jjMoveStringLiteralDfa2_0(active0, 0x2a8000000L); + case 84: + if ((active0 & 0x2000000L) != 0L) + return jjStopAtPos(1, 25); + break; + case 97: + return jjMoveStringLiteralDfa2_0(active0, 0xaa00L); + case 99: + return jjMoveStringLiteralDfa2_0(active0, 0x100000L); + case 101: + return jjMoveStringLiteralDfa2_0(active0, 0x481040L); + case 104: + return jjMoveStringLiteralDfa2_0(active0, 0x80L); + case 111: + return jjMoveStringLiteralDfa2_0(active0, 0x200010L); + case 112: + return jjMoveStringLiteralDfa2_0(active0, 0x4000L); + case 114: + return jjMoveStringLiteralDfa2_0(active0, 0x100L); + case 117: + return jjMoveStringLiteralDfa2_0(active0, 0x70420L); + default : + break; + } + return jjStartNfa_0(0, active0); +} +private final int jjMoveStringLiteralDfa2_0(long old0, long active0) +{ + if (((active0 &= old0)) == 0L) + return jjStartNfa_0(0, old0); + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { + jjStopStringLiteralDfa_0(1, active0); + return 2; + } + switch(curChar) + { + case 84: + if ((active0 & 0x4000000L) != 0L) + return jjStopAtPos(2, 26); + else if ((active0 & 0x8000000L) != 0L) + return jjStopAtPos(2, 27); + else if ((active0 & 0x10000000L) != 0L) + return jjStopAtPos(2, 28); + else if ((active0 & 0x20000000L) != 0L) + return jjStopAtPos(2, 29); + else if ((active0 & 0x40000000L) != 0L) + return jjStopAtPos(2, 30); + else if ((active0 & 0x80000000L) != 0L) + return jjStopAtPos(2, 31); + else if ((active0 & 0x100000000L) != 0L) + return jjStopAtPos(2, 32); + else if ((active0 & 0x200000000L) != 0L) + return jjStopAtPos(2, 33); + else if ((active0 & 0x400000000L) != 0L) + return jjStopAtPos(2, 34); + break; + case 98: + if ((active0 & 0x1000L) != 0L) + return jjStopAtPos(2, 12); + break; + case 99: + if ((active0 & 0x400000L) != 0L) + return jjStopAtPos(2, 22); + break; + case 100: + if ((active0 & 0x40L) != 0L) + return jjStopAtPos(2, 6); + break; + case 101: + if ((active0 & 0x20L) != 0L) + return jjStopAtPos(2, 5); + break; + case 103: + if ((active0 & 0x40000L) != 0L) + return jjStopAtPos(2, 18); + break; + case 105: + if ((active0 & 0x100L) != 0L) + return jjStopAtPos(2, 8); + break; + case 108: + if ((active0 & 0x20000L) != 0L) + return jjStopAtPos(2, 17); + break; + case 110: + if ((active0 & 0x10L) != 0L) + return jjStopAtPos(2, 4); + else if ((active0 & 0x400L) != 0L) + return jjStopAtPos(2, 10); + else if ((active0 & 0x800L) != 0L) + return jjStopAtPos(2, 11); + else if ((active0 & 0x10000L) != 0L) + return jjStopAtPos(2, 16); + break; + case 112: + if ((active0 & 0x80000L) != 0L) + return jjStopAtPos(2, 19); + break; + case 114: + if ((active0 & 0x2000L) != 0L) + return jjStopAtPos(2, 13); + else if ((active0 & 0x4000L) != 0L) + return jjStopAtPos(2, 14); + break; + case 116: + if ((active0 & 0x200L) != 0L) + return jjStopAtPos(2, 9); + else if ((active0 & 0x100000L) != 0L) + return jjStopAtPos(2, 20); + break; + case 117: + if ((active0 & 0x80L) != 0L) + return jjStopAtPos(2, 7); + break; + case 118: + if ((active0 & 0x200000L) != 0L) + return jjStopAtPos(2, 21); + break; + case 121: + if ((active0 & 0x8000L) != 0L) + return jjStopAtPos(2, 15); + break; + default : + break; + } + return jjStartNfa_0(1, active0); +} +private final void jjCheckNAdd(int state) +{ + if (jjrounds[state] != jjround) + { + jjstateSet[jjnewStateCnt++] = state; + jjrounds[state] = jjround; + } +} +private final void jjAddStates(int start, int end) +{ + do { + jjstateSet[jjnewStateCnt++] = jjnextStates[start]; + } while (start++ != end); +} +private final void jjCheckNAddTwoStates(int state1, int state2) +{ + jjCheckNAdd(state1); + jjCheckNAdd(state2); +} +private final void jjCheckNAddStates(int start, int end) +{ + do { + jjCheckNAdd(jjnextStates[start]); + } while (start++ != end); +} +private final void jjCheckNAddStates(int start) +{ + jjCheckNAdd(jjnextStates[start]); + jjCheckNAdd(jjnextStates[start + 1]); +} +private final int jjMoveNfa_0(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 4; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x3ff000000000000L & l) != 0L) + { + if (kind > 46) + kind = 46; + jjCheckNAdd(3); + } + else if ((0x100000200L & l) != 0L) + { + if (kind > 36) + kind = 36; + jjCheckNAdd(2); + } + else if ((0x280000000000L & l) != 0L) + { + if (kind > 24) + kind = 24; + } + break; + case 2: + if ((0x100000200L & l) == 0L) + break; + kind = 36; + jjCheckNAdd(2); + break; + case 3: + if ((0x3ff000000000000L & l) == 0L) + break; + kind = 46; + jjCheckNAdd(3); + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((0x7fffbfe07fffbfeL & l) != 0L) + kind = 35; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 4 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_1(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_1(int pos, long active0) +{ + return jjMoveNfa_1(jjStopStringLiteralDfa_1(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_1(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_1(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_1() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 40); + case 41: + return jjStopAtPos(0, 38); + default : + return jjMoveNfa_1(0, 0); + } +} +static final long[] jjbitVec0 = { + 0x0L, 0x0L, 0xffffffffffffffffL, 0xffffffffffffffffL +}; +private final int jjMoveNfa_1(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 41) + kind = 41; + break; + case 1: + if (kind > 39) + kind = 39; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 41) + kind = 41; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 39) + kind = 39; + break; + case 2: + if (kind > 41) + kind = 41; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 41) + kind = 41; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 39) + kind = 39; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +private final int jjStopStringLiteralDfa_2(int pos, long active0) +{ + switch (pos) + { + default : + return -1; + } +} +private final int jjStartNfa_2(int pos, long active0) +{ + return jjMoveNfa_2(jjStopStringLiteralDfa_2(pos, active0), pos + 1); +} +private final int jjStartNfaWithStates_2(int pos, int kind, int state) +{ + jjmatchedKind = kind; + jjmatchedPos = pos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return pos + 1; } + return jjMoveNfa_2(state, pos + 1); +} +private final int jjMoveStringLiteralDfa0_2() +{ + switch(curChar) + { + case 40: + return jjStopAtPos(0, 43); + case 41: + return jjStopAtPos(0, 44); + default : + return jjMoveNfa_2(0, 0); + } +} +private final int jjMoveNfa_2(int startState, int curPos) +{ + int[] nextStates; + int startsAt = 0; + jjnewStateCnt = 3; + int i = 1; + jjstateSet[0] = startState; + int j, kind = 0x7fffffff; + for (;;) + { + if (++jjround == 0x7fffffff) + ReInitRounds(); + if (curChar < 64) + { + long l = 1L << curChar; + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 45) + kind = 45; + break; + case 1: + if (kind > 42) + kind = 42; + break; + default : break; + } + } while(i != startsAt); + } + else if (curChar < 128) + { + long l = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if (kind > 45) + kind = 45; + if (curChar == 92) + jjstateSet[jjnewStateCnt++] = 1; + break; + case 1: + if (kind > 42) + kind = 42; + break; + case 2: + if (kind > 45) + kind = 45; + break; + default : break; + } + } while(i != startsAt); + } + else + { + int i2 = (curChar & 0xff) >> 6; + long l2 = 1L << (curChar & 077); + MatchLoop: do + { + switch(jjstateSet[--i]) + { + case 0: + if ((jjbitVec0[i2] & l2) != 0L && kind > 45) + kind = 45; + break; + case 1: + if ((jjbitVec0[i2] & l2) != 0L && kind > 42) + kind = 42; + break; + default : break; + } + } while(i != startsAt); + } + if (kind != 0x7fffffff) + { + jjmatchedKind = kind; + jjmatchedPos = curPos; + kind = 0x7fffffff; + } + ++curPos; + if ((i = jjnewStateCnt) == (startsAt = 3 - (jjnewStateCnt = startsAt))) + return curPos; + try { curChar = input_stream.readChar(); } + catch(java.io.IOException e) { return curPos; } + } +} +static final int[] jjnextStates = { +}; +public static final String[] jjstrLiteralImages = { +"", "\15", "\12", "\54", "\115\157\156", "\124\165\145", "\127\145\144", +"\124\150\165", "\106\162\151", "\123\141\164", "\123\165\156", "\112\141\156", +"\106\145\142", "\115\141\162", "\101\160\162", "\115\141\171", "\112\165\156", +"\112\165\154", "\101\165\147", "\123\145\160", "\117\143\164", "\116\157\166", +"\104\145\143", "\72", null, "\125\124", "\107\115\124", "\105\123\124", "\105\104\124", +"\103\123\124", "\103\104\124", "\115\123\124", "\115\104\124", "\120\123\124", +"\120\104\124", null, null, null, null, null, null, null, null, null, null, null, null, null, +null, }; +public static final String[] lexStateNames = { + "DEFAULT", + "INCOMMENT", + "NESTED_COMMENT", +}; +public static final int[] jjnewLexState = { + -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, 0, -1, 2, -1, -1, -1, -1, -1, -1, -1, -1, +}; +static final long[] jjtoToken = { + 0x400fffffffffL, +}; +static final long[] jjtoSkip = { + 0x5000000000L, +}; +static final long[] jjtoSpecial = { + 0x1000000000L, +}; +static final long[] jjtoMore = { + 0x3fa000000000L, +}; +protected SimpleCharStream input_stream; +private final int[] jjrounds = new int[4]; +private final int[] jjstateSet = new int[8]; +StringBuffer image; +int jjimageLen; +int lengthOfMatch; +protected char curChar; +public DateTimeParserTokenManager(SimpleCharStream stream){ + if (SimpleCharStream.staticFlag) + throw new Error("ERROR: Cannot use a static CharStream class with a non-static lexical analyzer."); + input_stream = stream; +} +public DateTimeParserTokenManager(SimpleCharStream stream, int lexState){ + this(stream); + SwitchTo(lexState); +} +public void ReInit(SimpleCharStream stream) +{ + jjmatchedPos = jjnewStateCnt = 0; + curLexState = defaultLexState; + input_stream = stream; + ReInitRounds(); +} +private final void ReInitRounds() +{ + int i; + jjround = 0x80000001; + for (i = 4; i-- > 0;) + jjrounds[i] = 0x80000000; +} +public void ReInit(SimpleCharStream stream, int lexState) +{ + ReInit(stream); + SwitchTo(lexState); +} +public void SwitchTo(int lexState) +{ + if (lexState >= 3 || lexState < 0) + throw new TokenMgrError("Error: Ignoring invalid lexical state : " + lexState + ". State unchanged.", TokenMgrError.INVALID_LEXICAL_STATE); + else + curLexState = lexState; +} + +protected Token jjFillToken() +{ + Token t = Token.newToken(jjmatchedKind); + t.kind = jjmatchedKind; + String im = jjstrLiteralImages[jjmatchedKind]; + t.image = (im == null) ? input_stream.GetImage() : im; + t.beginLine = input_stream.getBeginLine(); + t.beginColumn = input_stream.getBeginColumn(); + t.endLine = input_stream.getEndLine(); + t.endColumn = input_stream.getEndColumn(); + return t; +} + +int curLexState = 0; +int defaultLexState = 0; +int jjnewStateCnt; +int jjround; +int jjmatchedPos; +int jjmatchedKind; + +public Token getNextToken() +{ + int kind; + Token specialToken = null; + Token matchedToken; + int curPos = 0; + + EOFLoop : + for (;;) + { + try + { + curChar = input_stream.BeginToken(); + } + catch(java.io.IOException e) + { + jjmatchedKind = 0; + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + return matchedToken; + } + image = null; + jjimageLen = 0; + + for (;;) + { + switch(curLexState) + { + case 0: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_0(); + break; + case 1: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_1(); + break; + case 2: + jjmatchedKind = 0x7fffffff; + jjmatchedPos = 0; + curPos = jjMoveStringLiteralDfa0_2(); + break; + } + if (jjmatchedKind != 0x7fffffff) + { + if (jjmatchedPos + 1 < curPos) + input_stream.backup(curPos - jjmatchedPos - 1); + if ((jjtoToken[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + matchedToken.specialToken = specialToken; + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + return matchedToken; + } + else if ((jjtoSkip[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + if ((jjtoSpecial[jjmatchedKind >> 6] & (1L << (jjmatchedKind & 077))) != 0L) + { + matchedToken = jjFillToken(); + if (specialToken == null) + specialToken = matchedToken; + else + { + matchedToken.specialToken = specialToken; + specialToken = (specialToken.next = matchedToken); + } + } + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + continue EOFLoop; + } + MoreLexicalActions(); + if (jjnewLexState[jjmatchedKind] != -1) + curLexState = jjnewLexState[jjmatchedKind]; + curPos = 0; + jjmatchedKind = 0x7fffffff; + try { + curChar = input_stream.readChar(); + continue; + } + catch (java.io.IOException e1) { } + } + int error_line = input_stream.getEndLine(); + int error_column = input_stream.getEndColumn(); + String error_after = null; + boolean EOFSeen = false; + try { input_stream.readChar(); input_stream.backup(1); } + catch (java.io.IOException e1) { + EOFSeen = true; + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + if (curChar == '\n' || curChar == '\r') { + error_line++; + error_column = 0; + } + else + error_column++; + } + if (!EOFSeen) { + input_stream.backup(1); + error_after = curPos <= 1 ? "" : input_stream.GetImage(); + } + throw new TokenMgrError(EOFSeen, curLexState, error_line, error_column, error_after, curChar, TokenMgrError.LEXICAL_ERROR); + } + } +} + +void MoreLexicalActions() +{ + jjimageLen += (lengthOfMatch = jjmatchedPos + 1); + switch(jjmatchedKind) + { + case 39 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 40 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + commentNest = 1; + break; + case 42 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + image.deleteCharAt(image.length() - 2); + break; + case 43 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + ++commentNest; + break; + case 44 : + if (image == null) + image = new StringBuffer(); + image.append(input_stream.GetSuffix(jjimageLen)); + jjimageLen = 0; + --commentNest; if (commentNest == 0) SwitchTo(INCOMMENT); + break; + default : + break; + } +} +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java new file mode 100644 index 000000000..13b3ff097 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java @@ -0,0 +1,207 @@ +/* Generated By:JavaCC: Do not edit this line. ParseException.java Version 3.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.datetime.parser; + +/** + * This exception is thrown when parse errors are encountered. + * You can explicitly create objects of this exception type by + * calling the method generateParseException in the generated + * parser. + * + * You can modify this class to customize your error reporting + * mechanisms so long as you retain the public fields. + */ +public class ParseException extends Exception { + + /** + * This constructor is used by the method "generateParseException" + * in the generated parser. Calling this constructor generates + * a new object of this type with the fields "currentToken", + * "expectedTokenSequences", and "tokenImage" set. The boolean + * flag "specialConstructor" is also set to true to indicate that + * this constructor was used to create this object. + * This constructor calls its super class with the empty string + * to force the "toString" method of parent class "Throwable" to + * print the error message in the form: + * ParseException: <result of getMessage> + */ + public ParseException(Token currentTokenVal, + int[][] expectedTokenSequencesVal, + String[] tokenImageVal + ) + { + super(""); + specialConstructor = true; + currentToken = currentTokenVal; + expectedTokenSequences = expectedTokenSequencesVal; + tokenImage = tokenImageVal; + } + + /** + * The following constructors are for use by you for whatever + * purpose you can think of. Constructing the exception in this + * manner makes the exception behave in the normal way - i.e., as + * documented in the class "Throwable". The fields "errorToken", + * "expectedTokenSequences", and "tokenImage" do not contain + * relevant information. The JavaCC generated code does not use + * these constructors. + */ + + public ParseException() { + super(); + specialConstructor = false; + } + + public ParseException(String message) { + super(message); + specialConstructor = false; + } + + /** + * This variable determines which constructor was used to create + * this object and thereby affects the semantics of the + * "getMessage" method (see below). + */ + protected boolean specialConstructor; + + /** + * This is the last token that has been consumed successfully. If + * this object has been created due to a parse error, the token + * followng this token will (therefore) be the first error token. + */ + public Token currentToken; + + /** + * Each entry in this array is an array of integers. Each array + * of integers represents a sequence of tokens (by their ordinal + * values) that is expected at this point of the parse. + */ + public int[][] expectedTokenSequences; + + /** + * This is a reference to the "tokenImage" array of the generated + * parser within which the parse error occurred. This array is + * defined in the generated ...Constants interface. + */ + public String[] tokenImage; + + /** + * This method has the standard behavior when this object has been + * created using the standard constructors. Otherwise, it uses + * "currentToken" and "expectedTokenSequences" to generate a parse + * error message and returns it. If this object has been created + * due to a parse error, and you do not catch it (it gets thrown + * from the parser), then this method is called during the printing + * of the final stack trace, and hence the correct error message + * gets displayed. + */ + public String getMessage() { + if (!specialConstructor) { + return super.getMessage(); + } + StringBuffer expected = new StringBuffer(); + int maxSize = 0; + for (int i = 0; i < expectedTokenSequences.length; i++) { + if (maxSize < expectedTokenSequences[i].length) { + maxSize = expectedTokenSequences[i].length; + } + for (int j = 0; j < expectedTokenSequences[i].length; j++) { + expected.append(tokenImage[expectedTokenSequences[i][j]]).append(" "); + } + if (expectedTokenSequences[i][expectedTokenSequences[i].length - 1] != 0) { + expected.append("..."); + } + expected.append(eol).append(" "); + } + String retval = "Encountered \""; + Token tok = currentToken.next; + for (int i = 0; i < maxSize; i++) { + if (i != 0) retval += " "; + if (tok.kind == 0) { + retval += tokenImage[0]; + break; + } + retval += add_escapes(tok.image); + tok = tok.next; + } + retval += "\" at line " + currentToken.next.beginLine + ", column " + currentToken.next.beginColumn; + retval += "." + eol; + if (expectedTokenSequences.length == 1) { + retval += "Was expecting:" + eol + " "; + } else { + retval += "Was expecting one of:" + eol + " "; + } + retval += expected.toString(); + return retval; + } + + /** + * The end of line string for this machine. + */ + protected String eol = System.getProperty("line.separator", "\n"); + + /** + * Used to convert raw characters to their escaped version + * when these raw version cannot be used as part of an ASCII + * string literal. + */ + protected String add_escapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java new file mode 100644 index 000000000..2724529f7 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java @@ -0,0 +1,454 @@ +/* Generated By:JavaCC: Do not edit this line. SimpleCharStream.java Version 4.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.datetime.parser; + +/** + * An implementation of interface CharStream, where the stream is assumed to + * contain only ASCII characters (without unicode processing). + */ + +public class SimpleCharStream +{ + public static final boolean staticFlag = false; + int bufsize; + int available; + int tokenBegin; + public int bufpos = -1; + protected int bufline[]; + protected int bufcolumn[]; + + protected int column = 0; + protected int line = 1; + + protected boolean prevCharIsCR = false; + protected boolean prevCharIsLF = false; + + protected java.io.Reader inputStream; + + protected char[] buffer; + protected int maxNextCharInd = 0; + protected int inBuf = 0; + protected int tabSize = 8; + + protected void setTabSize(int i) { tabSize = i; } + protected int getTabSize(int i) { return tabSize; } + + + protected void ExpandBuff(boolean wrapAround) + { + char[] newbuffer = new char[bufsize + 2048]; + int newbufline[] = new int[bufsize + 2048]; + int newbufcolumn[] = new int[bufsize + 2048]; + + try + { + if (wrapAround) + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + System.arraycopy(buffer, 0, newbuffer, + bufsize - tokenBegin, bufpos); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + System.arraycopy(bufline, 0, newbufline, bufsize - tokenBegin, bufpos); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + System.arraycopy(bufcolumn, 0, newbufcolumn, bufsize - tokenBegin, bufpos); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos += (bufsize - tokenBegin)); + } + else + { + System.arraycopy(buffer, tokenBegin, newbuffer, 0, bufsize - tokenBegin); + buffer = newbuffer; + + System.arraycopy(bufline, tokenBegin, newbufline, 0, bufsize - tokenBegin); + bufline = newbufline; + + System.arraycopy(bufcolumn, tokenBegin, newbufcolumn, 0, bufsize - tokenBegin); + bufcolumn = newbufcolumn; + + maxNextCharInd = (bufpos -= tokenBegin); + } + } + catch (Throwable t) + { + throw new Error(t.getMessage()); + } + + + bufsize += 2048; + available = bufsize; + tokenBegin = 0; + } + + protected void FillBuff() throws java.io.IOException + { + if (maxNextCharInd == available) + { + if (available == bufsize) + { + if (tokenBegin > 2048) + { + bufpos = maxNextCharInd = 0; + available = tokenBegin; + } + else if (tokenBegin < 0) + bufpos = maxNextCharInd = 0; + else + ExpandBuff(false); + } + else if (available > tokenBegin) + available = bufsize; + else if ((tokenBegin - available) < 2048) + ExpandBuff(true); + else + available = tokenBegin; + } + + int i; + try { + if ((i = inputStream.read(buffer, maxNextCharInd, + available - maxNextCharInd)) == -1) + { + inputStream.close(); + throw new java.io.IOException(); + } + else + maxNextCharInd += i; + return; + } + catch(java.io.IOException e) { + --bufpos; + backup(0); + if (tokenBegin == -1) + tokenBegin = bufpos; + throw e; + } + } + + public char BeginToken() throws java.io.IOException + { + tokenBegin = -1; + char c = readChar(); + tokenBegin = bufpos; + + return c; + } + + protected void UpdateLineColumn(char c) + { + column++; + + if (prevCharIsLF) + { + prevCharIsLF = false; + line += (column = 1); + } + else if (prevCharIsCR) + { + prevCharIsCR = false; + if (c == '\n') + { + prevCharIsLF = true; + } + else + line += (column = 1); + } + + switch (c) + { + case '\r' : + prevCharIsCR = true; + break; + case '\n' : + prevCharIsLF = true; + break; + case '\t' : + column--; + column += (tabSize - (column % tabSize)); + break; + default : + break; + } + + bufline[bufpos] = line; + bufcolumn[bufpos] = column; + } + + public char readChar() throws java.io.IOException + { + if (inBuf > 0) + { + --inBuf; + + if (++bufpos == bufsize) + bufpos = 0; + + return buffer[bufpos]; + } + + if (++bufpos >= maxNextCharInd) + FillBuff(); + + char c = buffer[bufpos]; + + UpdateLineColumn(c); + return (c); + } + + /** + * @deprecated + * @see #getEndColumn + */ + @Deprecated + public int getColumn() { + return bufcolumn[bufpos]; + } + + /** + * @deprecated + * @see #getEndLine + */ + @Deprecated + public int getLine() { + return bufline[bufpos]; + } + + public int getEndColumn() { + return bufcolumn[bufpos]; + } + + public int getEndLine() { + return bufline[bufpos]; + } + + public int getBeginColumn() { + return bufcolumn[tokenBegin]; + } + + public int getBeginLine() { + return bufline[tokenBegin]; + } + + public void backup(int amount) { + + inBuf += amount; + if ((bufpos -= amount) < 0) + bufpos += bufsize; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + + public SimpleCharStream(java.io.Reader dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.Reader dstream) + { + this(dstream, 1, 1, 4096); + } + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn, int buffersize) + { + inputStream = dstream; + line = startline; + column = startcolumn - 1; + + if (buffer == null || buffersize != buffer.length) + { + available = bufsize = buffersize; + buffer = new char[buffersize]; + bufline = new int[buffersize]; + bufcolumn = new int[buffersize]; + } + prevCharIsLF = prevCharIsCR = false; + tokenBegin = inBuf = maxNextCharInd = 0; + bufpos = -1; + } + + public void ReInit(java.io.Reader dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + + public void ReInit(java.io.Reader dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + this(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + this(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, int startline, + int startcolumn) + { + this(dstream, startline, startcolumn, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + this(dstream, encoding, 1, 1, 4096); + } + + public SimpleCharStream(java.io.InputStream dstream) + { + this(dstream, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn, int buffersize) throws java.io.UnsupportedEncodingException + { + ReInit(encoding == null ? new java.io.InputStreamReader(dstream) : new java.io.InputStreamReader(dstream, encoding), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn, int buffersize) + { + ReInit(new java.io.InputStreamReader(dstream), startline, startcolumn, buffersize); + } + + public void ReInit(java.io.InputStream dstream, String encoding) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, 1, 1, 4096); + } + + public void ReInit(java.io.InputStream dstream) + { + ReInit(dstream, 1, 1, 4096); + } + public void ReInit(java.io.InputStream dstream, String encoding, int startline, + int startcolumn) throws java.io.UnsupportedEncodingException + { + ReInit(dstream, encoding, startline, startcolumn, 4096); + } + public void ReInit(java.io.InputStream dstream, int startline, + int startcolumn) + { + ReInit(dstream, startline, startcolumn, 4096); + } + public String GetImage() + { + if (bufpos >= tokenBegin) + return new String(buffer, tokenBegin, bufpos - tokenBegin + 1); + else + return new String(buffer, tokenBegin, bufsize - tokenBegin) + + new String(buffer, 0, bufpos + 1); + } + + public char[] GetSuffix(int len) + { + char[] ret = new char[len]; + + if ((bufpos + 1) >= len) + System.arraycopy(buffer, bufpos - len + 1, ret, 0, len); + else + { + System.arraycopy(buffer, bufsize - (len - bufpos - 1), ret, 0, + len - bufpos - 1); + System.arraycopy(buffer, 0, ret, len - bufpos - 1, bufpos + 1); + } + + return ret; + } + + public void Done() + { + buffer = null; + bufline = null; + bufcolumn = null; + } + + /** + * Method to adjust line and column numbers for the start of a token. + */ + public void adjustBeginLineColumn(int newLine, int newCol) + { + int start = tokenBegin; + int len; + + if (bufpos >= tokenBegin) + { + len = bufpos - tokenBegin + inBuf + 1; + } + else + { + len = bufsize - tokenBegin + bufpos + 1 + inBuf; + } + + int i = 0, j = 0, k = 0; + int nextColDiff = 0, columnDiff = 0; + + while (i < len && + bufline[j = start % bufsize] == bufline[k = ++start % bufsize]) + { + bufline[j] = newLine; + nextColDiff = columnDiff + bufcolumn[k] - bufcolumn[j]; + bufcolumn[j] = newCol + columnDiff; + columnDiff = nextColDiff; + i++; + } + + if (i < len) + { + bufline[j] = newLine++; + bufcolumn[j] = newCol + columnDiff; + + while (i++ < len) + { + if (bufline[j = start % bufsize] != bufline[++start % bufsize]) + bufline[j] = newLine++; + else + bufline[j] = newLine; + } + } + + line = bufline[j]; + column = bufcolumn[j]; + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java new file mode 100644 index 000000000..0927a0921 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java @@ -0,0 +1,96 @@ +/* Generated By:JavaCC: Do not edit this line. Token.java Version 3.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.datetime.parser; + +/** + * Describes the input token stream. + */ + +public class Token { + + /** + * An integer that describes the kind of this token. This numbering + * system is determined by JavaCCParser, and a table of these numbers is + * stored in the file ...Constants.java. + */ + public int kind; + + /** + * beginLine and beginColumn describe the position of the first character + * of this token; endLine and endColumn describe the position of the + * last character of this token. + */ + public int beginLine, beginColumn, endLine, endColumn; + + /** + * The string image of the token. + */ + public String image; + + /** + * A reference to the next regular (non-special) token from the input + * stream. If this is the last token from the input stream, or if the + * token manager has not read tokens beyond this one, this field is + * set to null. This is true only if this token is also a regular + * token. Otherwise, see below for a description of the contents of + * this field. + */ + public Token next; + + /** + * This field is used to access special tokens that occur prior to this + * token, but after the immediately preceding regular (non-special) token. + * If there are no such special tokens, this field is set to null. + * When there are more than one such special token, this field refers + * to the last of these special tokens, which in turn refers to the next + * previous special token through its specialToken field, and so on + * until the first special token (whose specialToken field is null). + * The next fields of special tokens refer to other special tokens that + * immediately follow it (without an intervening regular token). If there + * is no such token, this field is null. + */ + public Token specialToken; + + /** + * Returns the image. + */ + public String toString() + { + return image; + } + + /** + * Returns a new Token object, by default. However, if you want, you + * can create and return subclass objects based on the value of ofKind. + * Simply add the cases to the switch for all those special cases. + * For example, if you have a subclass of Token called IDToken that + * you want to create if ofKind is ID, simlpy add something like : + * + * case MyParserConstants.ID : return new IDToken(); + * + * to the following switch statement. Then you can cast matchedToken + * variable to the appropriate type and use it in your lexical actions. + */ + public static final Token newToken(int ofKind) + { + switch(ofKind) + { + default : return new Token(); + } + } + +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java new file mode 100644 index 000000000..e7043c1b7 --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java @@ -0,0 +1,148 @@ +/* Generated By:JavaCC: Do not edit this line. TokenMgrError.java Version 3.0 */ +/* + * Copyright 2004 the mime4j 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 org.apache.james.mime4j.field.datetime.parser; + +public class TokenMgrError extends Error +{ + /* + * Ordinals for various reasons why an Error of this type can be thrown. + */ + + /** + * Lexical error occured. + */ + static final int LEXICAL_ERROR = 0; + + /** + * An attempt wass made to create a second instance of a static token manager. + */ + static final int STATIC_LEXER_ERROR = 1; + + /** + * Tried to change to an invalid lexical state. + */ + static final int INVALID_LEXICAL_STATE = 2; + + /** + * Detected (and bailed out of) an infinite loop in the token manager. + */ + static final int LOOP_DETECTED = 3; + + /** + * Indicates the reason why the exception is thrown. It will have + * one of the above 4 values. + */ + int errorCode; + + /** + * Replaces unprintable characters by their espaced (or unicode escaped) + * equivalents in the given string + */ + protected static final String addEscapes(String str) { + StringBuffer retval = new StringBuffer(); + char ch; + for (int i = 0; i < str.length(); i++) { + switch (str.charAt(i)) + { + case 0 : + continue; + case '\b': + retval.append("\\b"); + continue; + case '\t': + retval.append("\\t"); + continue; + case '\n': + retval.append("\\n"); + continue; + case '\f': + retval.append("\\f"); + continue; + case '\r': + retval.append("\\r"); + continue; + case '\"': + retval.append("\\\""); + continue; + case '\'': + retval.append("\\\'"); + continue; + case '\\': + retval.append("\\\\"); + continue; + default: + if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) { + String s = "0000" + Integer.toString(ch, 16); + retval.append("\\u" + s.substring(s.length() - 4, s.length())); + } else { + retval.append(ch); + } + continue; + } + } + return retval.toString(); + } + + /** + * Returns a detailed message for the Error when it is thrown by the + * token manager to indicate a lexical error. + * Parameters : + * EOFSeen : indicates if EOF caused the lexicl error + * curLexState : lexical state in which this error occured + * errorLine : line number when the error occured + * errorColumn : column number when the error occured + * errorAfter : prefix that was seen before this error occured + * curchar : the offending character + * Note: You can customize the lexical error message by modifying this method. + */ + protected static String LexicalError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar) { + return("Lexical error at line " + + errorLine + ", column " + + errorColumn + ". Encountered: " + + (EOFSeen ? "<EOF> " : ("\"" + addEscapes(String.valueOf(curChar)) + "\"") + " (" + (int)curChar + "), ") + + "after : \"" + addEscapes(errorAfter) + "\""); + } + + /** + * You can also modify the body of this method to customize your error messages. + * For example, cases like LOOP_DETECTED and INVALID_LEXICAL_STATE are not + * of end-users concern, so you can return something like : + * + * "Internal Error : Please file a bug report .... " + * + * from this method for such cases in the release version of your parser. + */ + public String getMessage() { + return super.getMessage(); + } + + /* + * Constructors of various flavors follow. + */ + + public TokenMgrError() { + } + + public TokenMgrError(String message, int reason) { + super(message); + errorCode = reason; + } + + public TokenMgrError(boolean EOFSeen, int lexState, int errorLine, int errorColumn, String errorAfter, char curChar, int reason) { + this(LexicalError(EOFSeen, lexState, errorLine, errorColumn, errorAfter, curChar), reason); + } +} diff --git a/java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java new file mode 100644 index 000000000..4e712fcdd --- /dev/null +++ b/java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java @@ -0,0 +1,1249 @@ +/**************************************************************** + * 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 org.apache.james.mime4j.util; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.IllegalCharsetNameException; +import java.nio.charset.UnsupportedCharsetException; +import java.util.HashMap; +import java.util.Locale; +import java.util.TreeSet; + +//BEGIN android-changed: Stubbing out logging +import org.apache.james.mime4j.Log; +import org.apache.james.mime4j.LogFactory; +//END android-changed + +/** + * Utility class for working with character sets. It is somewhat similar to + * the Java 1.4 <code>java.nio.charset.Charset</code> class but knows many + * more aliases and is compatible with Java 1.3. It will use a simple detection + * mechanism to detect what character sets the current VM supports. This will + * be a sub-set of the character sets listed in the + * <a href="http://java.sun.com/j2se/1.5.0/docs/guide/intl/encoding.doc.html"> + * Java 1.5 (J2SE5.0) Supported Encodings</a> document. + * <p> + * The <a href="http://www.iana.org/assignments/character-sets"> + * IANA Character Sets</a> document has been used to determine the preferred + * MIME character set names and to get a list of known aliases. + * <p> + * This is a complete list of the character sets known to this class: + * <table> + * <tr> + * <td>Canonical (Java) name</td> + * <td>MIME preferred</td> + * <td>Aliases</td> + * </tr> + * <tr> + * <td>ASCII</td> + * <td>US-ASCII</td> + * <td>ANSI_X3.4-1968 iso-ir-6 ANSI_X3.4-1986 ISO_646.irv:1991 ISO646-US us IBM367 cp367 csASCII ascii7 646 iso_646.irv:1983 </td> + * </tr> + * <tr> + * <td>Big5</td> + * <td>Big5</td> + * <td>csBig5 CN-Big5 BIG-FIVE BIGFIVE </td> + * </tr> + * <tr> + * <td>Big5_HKSCS</td> + * <td>Big5-HKSCS</td> + * <td>big5hkscs </td> + * </tr> + * <tr> + * <td>Big5_Solaris</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp037</td> + * <td>IBM037</td> + * <td>ebcdic-cp-us ebcdic-cp-ca ebcdic-cp-wt ebcdic-cp-nl csIBM037 </td> + * </tr> + * <tr> + * <td>Cp1006</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1025</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1026</td> + * <td>IBM1026</td> + * <td>csIBM1026 </td> + * </tr> + * <tr> + * <td>Cp1046</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1047</td> + * <td>IBM1047</td> + * <td>IBM-1047 </td> + * </tr> + * <tr> + * <td>Cp1097</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1098</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1112</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1122</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1123</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1124</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1140</td> + * <td>IBM01140</td> + * <td>CCSID01140 CP01140 ebcdic-us-37+euro </td> + * </tr> + * <tr> + * <td>Cp1141</td> + * <td>IBM01141</td> + * <td>CCSID01141 CP01141 ebcdic-de-273+euro </td> + * </tr> + * <tr> + * <td>Cp1142</td> + * <td>IBM01142</td> + * <td>CCSID01142 CP01142 ebcdic-dk-277+euro ebcdic-no-277+euro </td> + * </tr> + * <tr> + * <td>Cp1143</td> + * <td>IBM01143</td> + * <td>CCSID01143 CP01143 ebcdic-fi-278+euro ebcdic-se-278+euro </td> + * </tr> + * <tr> + * <td>Cp1144</td> + * <td>IBM01144</td> + * <td>CCSID01144 CP01144 ebcdic-it-280+euro </td> + * </tr> + * <tr> + * <td>Cp1145</td> + * <td>IBM01145</td> + * <td>CCSID01145 CP01145 ebcdic-es-284+euro </td> + * </tr> + * <tr> + * <td>Cp1146</td> + * <td>IBM01146</td> + * <td>CCSID01146 CP01146 ebcdic-gb-285+euro </td> + * </tr> + * <tr> + * <td>Cp1147</td> + * <td>IBM01147</td> + * <td>CCSID01147 CP01147 ebcdic-fr-297+euro </td> + * </tr> + * <tr> + * <td>Cp1148</td> + * <td>IBM01148</td> + * <td>CCSID01148 CP01148 ebcdic-international-500+euro </td> + * </tr> + * <tr> + * <td>Cp1149</td> + * <td>IBM01149</td> + * <td>CCSID01149 CP01149 ebcdic-is-871+euro </td> + * </tr> + * <tr> + * <td>Cp1250</td> + * <td>windows-1250</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1251</td> + * <td>windows-1251</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1252</td> + * <td>windows-1252</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1253</td> + * <td>windows-1253</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1254</td> + * <td>windows-1254</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1255</td> + * <td>windows-1255</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1256</td> + * <td>windows-1256</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1257</td> + * <td>windows-1257</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1258</td> + * <td>windows-1258</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1381</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp1383</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp273</td> + * <td>IBM273</td> + * <td>csIBM273 </td> + * </tr> + * <tr> + * <td>Cp277</td> + * <td>IBM277</td> + * <td>EBCDIC-CP-DK EBCDIC-CP-NO csIBM277 </td> + * </tr> + * <tr> + * <td>Cp278</td> + * <td>IBM278</td> + * <td>CP278 ebcdic-cp-fi ebcdic-cp-se csIBM278 </td> + * </tr> + * <tr> + * <td>Cp280</td> + * <td>IBM280</td> + * <td>ebcdic-cp-it csIBM280 </td> + * </tr> + * <tr> + * <td>Cp284</td> + * <td>IBM284</td> + * <td>ebcdic-cp-es csIBM284 </td> + * </tr> + * <tr> + * <td>Cp285</td> + * <td>IBM285</td> + * <td>ebcdic-cp-gb csIBM285 </td> + * </tr> + * <tr> + * <td>Cp297</td> + * <td>IBM297</td> + * <td>ebcdic-cp-fr csIBM297 </td> + * </tr> + * <tr> + * <td>Cp33722</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp420</td> + * <td>IBM420</td> + * <td>ebcdic-cp-ar1 csIBM420 </td> + * </tr> + * <tr> + * <td>Cp424</td> + * <td>IBM424</td> + * <td>ebcdic-cp-he csIBM424 </td> + * </tr> + * <tr> + * <td>Cp437</td> + * <td>IBM437</td> + * <td>437 csPC8CodePage437 </td> + * </tr> + * <tr> + * <td>Cp500</td> + * <td>IBM500</td> + * <td>ebcdic-cp-be ebcdic-cp-ch csIBM500 </td> + * </tr> + * <tr> + * <td>Cp737</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp775</td> + * <td>IBM775</td> + * <td>csPC775Baltic </td> + * </tr> + * <tr> + * <td>Cp838</td> + * <td>IBM-Thai</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp850</td> + * <td>IBM850</td> + * <td>850 csPC850Multilingual </td> + * </tr> + * <tr> + * <td>Cp852</td> + * <td>IBM852</td> + * <td>852 csPCp852 </td> + * </tr> + * <tr> + * <td>Cp855</td> + * <td>IBM855</td> + * <td>855 csIBM855 </td> + * </tr> + * <tr> + * <td>Cp856</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp857</td> + * <td>IBM857</td> + * <td>857 csIBM857 </td> + * </tr> + * <tr> + * <td>Cp858</td> + * <td>IBM00858</td> + * <td>CCSID00858 CP00858 PC-Multilingual-850+euro </td> + * </tr> + * <tr> + * <td>Cp860</td> + * <td>IBM860</td> + * <td>860 csIBM860 </td> + * </tr> + * <tr> + * <td>Cp861</td> + * <td>IBM861</td> + * <td>861 cp-is csIBM861 </td> + * </tr> + * <tr> + * <td>Cp862</td> + * <td>IBM862</td> + * <td>862 csPC862LatinHebrew </td> + * </tr> + * <tr> + * <td>Cp863</td> + * <td>IBM863</td> + * <td>863 csIBM863 </td> + * </tr> + * <tr> + * <td>Cp864</td> + * <td>IBM864</td> + * <td>cp864 csIBM864 </td> + * </tr> + * <tr> + * <td>Cp865</td> + * <td>IBM865</td> + * <td>865 csIBM865 </td> + * </tr> + * <tr> + * <td>Cp866</td> + * <td>IBM866</td> + * <td>866 csIBM866 </td> + * </tr> + * <tr> + * <td>Cp868</td> + * <td>IBM868</td> + * <td>cp-ar csIBM868 </td> + * </tr> + * <tr> + * <td>Cp869</td> + * <td>IBM869</td> + * <td>cp-gr csIBM869 </td> + * </tr> + * <tr> + * <td>Cp870</td> + * <td>IBM870</td> + * <td>ebcdic-cp-roece ebcdic-cp-yu csIBM870 </td> + * </tr> + * <tr> + * <td>Cp871</td> + * <td>IBM871</td> + * <td>ebcdic-cp-is csIBM871 </td> + * </tr> + * <tr> + * <td>Cp875</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp918</td> + * <td>IBM918</td> + * <td>ebcdic-cp-ar2 csIBM918 </td> + * </tr> + * <tr> + * <td>Cp921</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp922</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp930</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp933</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp935</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp937</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp939</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp942</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp942C</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp943</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp943C</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp948</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp949</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp949C</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp950</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp964</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>Cp970</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>EUC_CN</td> + * <td>GB2312</td> + * <td>x-EUC-CN csGB2312 euccn euc-cn gb2312-80 gb2312-1980 CN-GB CN-GB-ISOIR165 </td> + * </tr> + * <tr> + * <td>EUC_JP</td> + * <td>EUC-JP</td> + * <td>csEUCPkdFmtJapanese Extended_UNIX_Code_Packed_Format_for_Japanese eucjis x-eucjp eucjp x-euc-jp </td> + * </tr> + * <tr> + * <td>EUC_JP_LINUX</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>EUC_JP_Solaris</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>EUC_KR</td> + * <td>EUC-KR</td> + * <td>csEUCKR ksc5601 5601 ksc5601_1987 ksc_5601 ksc5601-1987 ks_c_5601-1987 euckr </td> + * </tr> + * <tr> + * <td>EUC_TW</td> + * <td>EUC-TW</td> + * <td>x-EUC-TW cns11643 euctw </td> + * </tr> + * <tr> + * <td>GB18030</td> + * <td>GB18030</td> + * <td>gb18030-2000 </td> + * </tr> + * <tr> + * <td>GBK</td> + * <td>windows-936</td> + * <td>CP936 MS936 ms_936 x-mswin-936 </td> + * </tr> + * <tr> + * <td>ISCII91</td> + * <td>?</td> + * <td>x-ISCII91 iscii </td> + * </tr> + * <tr> + * <td>ISO2022CN</td> + * <td>ISO-2022-CN</td> + * <td></td> + * </tr> + * <tr> + * <td>ISO2022JP</td> + * <td>ISO-2022-JP</td> + * <td>csISO2022JP JIS jis_encoding csjisencoding </td> + * </tr> + * <tr> + * <td>ISO2022KR</td> + * <td>ISO-2022-KR</td> + * <td>csISO2022KR </td> + * </tr> + * <tr> + * <td>ISO2022_CN_CNS</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>ISO2022_CN_GB</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>ISO8859_1</td> + * <td>ISO-8859-1</td> + * <td>ISO_8859-1:1987 iso-ir-100 ISO_8859-1 latin1 l1 IBM819 CP819 csISOLatin1 8859_1 819 IBM-819 ISO8859-1 ISO_8859_1 </td> + * </tr> + * <tr> + * <td>ISO8859_13</td> + * <td>ISO-8859-13</td> + * <td></td> + * </tr> + * <tr> + * <td>ISO8859_15</td> + * <td>ISO-8859-15</td> + * <td>ISO_8859-15 Latin-9 8859_15 csISOlatin9 IBM923 cp923 923 L9 IBM-923 ISO8859-15 LATIN9 LATIN0 csISOlatin0 ISO8859_15_FDIS </td> + * </tr> + * <tr> + * <td>ISO8859_2</td> + * <td>ISO-8859-2</td> + * <td>ISO_8859-2:1987 iso-ir-101 ISO_8859-2 latin2 l2 csISOLatin2 8859_2 iso8859_2 </td> + * </tr> + * <tr> + * <td>ISO8859_3</td> + * <td>ISO-8859-3</td> + * <td>ISO_8859-3:1988 iso-ir-109 ISO_8859-3 latin3 l3 csISOLatin3 8859_3 </td> + * </tr> + * <tr> + * <td>ISO8859_4</td> + * <td>ISO-8859-4</td> + * <td>ISO_8859-4:1988 iso-ir-110 ISO_8859-4 latin4 l4 csISOLatin4 8859_4 </td> + * </tr> + * <tr> + * <td>ISO8859_5</td> + * <td>ISO-8859-5</td> + * <td>ISO_8859-5:1988 iso-ir-144 ISO_8859-5 cyrillic csISOLatinCyrillic 8859_5 </td> + * </tr> + * <tr> + * <td>ISO8859_6</td> + * <td>ISO-8859-6</td> + * <td>ISO_8859-6:1987 iso-ir-127 ISO_8859-6 ECMA-114 ASMO-708 arabic csISOLatinArabic 8859_6 </td> + * </tr> + * <tr> + * <td>ISO8859_7</td> + * <td>ISO-8859-7</td> + * <td>ISO_8859-7:1987 iso-ir-126 ISO_8859-7 ELOT_928 ECMA-118 greek greek8 csISOLatinGreek 8859_7 sun_eu_greek </td> + * </tr> + * <tr> + * <td>ISO8859_8</td> + * <td>ISO-8859-8</td> + * <td>ISO_8859-8:1988 iso-ir-138 ISO_8859-8 hebrew csISOLatinHebrew 8859_8 </td> + * </tr> + * <tr> + * <td>ISO8859_9</td> + * <td>ISO-8859-9</td> + * <td>ISO_8859-9:1989 iso-ir-148 ISO_8859-9 latin5 l5 csISOLatin5 8859_9 </td> + * </tr> + * <tr> + * <td>JISAutoDetect</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>JIS_C6626-1983</td> + * <td>JIS_C6626-1983</td> + * <td>x-JIS0208 JIS0208 csISO87JISX0208 x0208 JIS_X0208-1983 iso-ir-87 </td> + * </tr> + * <tr> + * <td>JIS_X0201</td> + * <td>JIS_X0201</td> + * <td>X0201 JIS0201 csHalfWidthKatakana </td> + * </tr> + * <tr> + * <td>JIS_X0212-1990</td> + * <td>JIS_X0212-1990</td> + * <td>iso-ir-159 x0212 JIS0212 csISO159JISX02121990 </td> + * </tr> + * <tr> + * <td>KOI8_R</td> + * <td>KOI8-R</td> + * <td>csKOI8R koi8 </td> + * </tr> + * <tr> + * <td>MS874</td> + * <td>windows-874</td> + * <td>cp874 </td> + * </tr> + * <tr> + * <td>MS932</td> + * <td>Windows-31J</td> + * <td>windows-932 csWindows31J x-ms-cp932 </td> + * </tr> + * <tr> + * <td>MS949</td> + * <td>windows-949</td> + * <td>windows949 ms_949 x-windows-949 </td> + * </tr> + * <tr> + * <td>MS950</td> + * <td>windows-950</td> + * <td>x-windows-950 </td> + * </tr> + * <tr> + * <td>MS950_HKSCS</td> + * <td></td> + * <td></td> + * </tr> + * <tr> + * <td>MacArabic</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>MacCentralEurope</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>MacCroatian</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>MacCyrillic</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>MacDingbat</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>MacGreek</td> + * <td>MacGreek</td> + * <td></td> + * </tr> + * <tr> + * <td>MacHebrew</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>MacIceland</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>MacRoman</td> + * <td>MacRoman</td> + * <td>Macintosh MAC csMacintosh </td> + * </tr> + * <tr> + * <td>MacRomania</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>MacSymbol</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>MacThai</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>MacTurkish</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>MacUkraine</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>SJIS</td> + * <td>Shift_JIS</td> + * <td>MS_Kanji csShiftJIS shift-jis x-sjis pck </td> + * </tr> + * <tr> + * <td>TIS620</td> + * <td>TIS-620</td> + * <td></td> + * </tr> + * <tr> + * <td>UTF-16</td> + * <td>UTF-16</td> + * <td>UTF_16 </td> + * </tr> + * <tr> + * <td>UTF8</td> + * <td>UTF-8</td> + * <td></td> + * </tr> + * <tr> + * <td>UnicodeBig</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>UnicodeBigUnmarked</td> + * <td>UTF-16BE</td> + * <td>X-UTF-16BE UTF_16BE ISO-10646-UCS-2 </td> + * </tr> + * <tr> + * <td>UnicodeLittle</td> + * <td>?</td> + * <td></td> + * </tr> + * <tr> + * <td>UnicodeLittleUnmarked</td> + * <td>UTF-16LE</td> + * <td>UTF_16LE X-UTF-16LE </td> + * </tr> + * <tr> + * <td>x-Johab</td> + * <td>johab</td> + * <td>johab cp1361 ms1361 ksc5601-1992 ksc5601_1992 </td> + * </tr> + * <tr> + * <td>x-iso-8859-11</td> + * <td>?</td> + * <td></td> + * </tr> + * </table> + * + * + * @version $Id: CharsetUtil.java,v 1.1 2004/10/25 07:26:46 ntherning Exp $ + */ +public class CharsetUtil { + private static Log log = LogFactory.getLog(CharsetUtil.class); + + private static class Charset implements Comparable<Charset> { + private String canonical = null; + private String mime = null; + private String[] aliases = null; + + private Charset(String canonical, String mime, String[] aliases) { + this.canonical = canonical; + this.mime = mime; + this.aliases = aliases; + } + + public int compareTo(Charset c) { + return this.canonical.compareTo(c.canonical); + } + } + + private static Charset[] JAVA_CHARSETS = { + new Charset("ISO8859_1", "ISO-8859-1", + new String[] {"ISO_8859-1:1987", "iso-ir-100", "ISO_8859-1", + "latin1", "l1", "IBM819", "CP819", + "csISOLatin1", "8859_1", "819", "IBM-819", + "ISO8859-1", "ISO_8859_1"}), + new Charset("ISO8859_2", "ISO-8859-2", + new String[] {"ISO_8859-2:1987", "iso-ir-101", "ISO_8859-2", + "latin2", "l2", "csISOLatin2", "8859_2", + "iso8859_2"}), + new Charset("ISO8859_3", "ISO-8859-3", new String[] {"ISO_8859-3:1988", "iso-ir-109", "ISO_8859-3", "latin3", "l3", "csISOLatin3", "8859_3"}), + new Charset("ISO8859_4", "ISO-8859-4", + new String[] {"ISO_8859-4:1988", "iso-ir-110", "ISO_8859-4", + "latin4", "l4", "csISOLatin4", "8859_4"}), + new Charset("ISO8859_5", "ISO-8859-5", + new String[] {"ISO_8859-5:1988", "iso-ir-144", "ISO_8859-5", + "cyrillic", "csISOLatinCyrillic", "8859_5"}), + new Charset("ISO8859_6", "ISO-8859-6", new String[] {"ISO_8859-6:1987", "iso-ir-127", "ISO_8859-6", "ECMA-114", "ASMO-708", "arabic", "csISOLatinArabic", "8859_6"}), + new Charset("ISO8859_7", "ISO-8859-7", + new String[] {"ISO_8859-7:1987", "iso-ir-126", "ISO_8859-7", + "ELOT_928", "ECMA-118", "greek", "greek8", + "csISOLatinGreek", "8859_7", "sun_eu_greek"}), + new Charset("ISO8859_8", "ISO-8859-8", new String[] {"ISO_8859-8:1988", "iso-ir-138", "ISO_8859-8", "hebrew", "csISOLatinHebrew", "8859_8"}), + new Charset("ISO8859_9", "ISO-8859-9", + new String[] {"ISO_8859-9:1989", "iso-ir-148", "ISO_8859-9", + "latin5", "l5", "csISOLatin5", "8859_9"}), + + new Charset("ISO8859_13", "ISO-8859-13", new String[] {}), + new Charset("ISO8859_15", "ISO-8859-15", + new String[] {"ISO_8859-15", "Latin-9", "8859_15", + "csISOlatin9", "IBM923", "cp923", "923", "L9", + "IBM-923", "ISO8859-15", "LATIN9", "LATIN0", + "csISOlatin0", "ISO8859_15_FDIS"}), + new Charset("KOI8_R", "KOI8-R", new String[] {"csKOI8R", "koi8"}), + new Charset("ASCII", "US-ASCII", + new String[] {"ANSI_X3.4-1968", "iso-ir-6", + "ANSI_X3.4-1986", "ISO_646.irv:1991", + "ISO646-US", "us", "IBM367", "cp367", + "csASCII", "ascii7", "646", "iso_646.irv:1983"}), + new Charset("UTF8", "UTF-8", new String[] {}), + new Charset("UTF-16", "UTF-16", new String[] {"UTF_16"}), + new Charset("UnicodeBigUnmarked", "UTF-16BE", new String[] {"X-UTF-16BE", "UTF_16BE", "ISO-10646-UCS-2"}), + new Charset("UnicodeLittleUnmarked", "UTF-16LE", new String[] {"UTF_16LE", "X-UTF-16LE"}), + new Charset("Big5", "Big5", new String[] {"csBig5", "CN-Big5", "BIG-FIVE", "BIGFIVE"}), + new Charset("Big5_HKSCS", "Big5-HKSCS", new String[] {"big5hkscs"}), + new Charset("EUC_JP", "EUC-JP", + new String[] {"csEUCPkdFmtJapanese", + "Extended_UNIX_Code_Packed_Format_for_Japanese", + "eucjis", "x-eucjp", "eucjp", "x-euc-jp"}), + new Charset("EUC_KR", "EUC-KR", + new String[] {"csEUCKR", "ksc5601", "5601", "ksc5601_1987", + "ksc_5601", "ksc5601-1987", "ks_c_5601-1987", + "euckr"}), + new Charset("GB18030", "GB18030", new String[] {"gb18030-2000"}), + new Charset("EUC_CN", "GB2312", new String[] {"x-EUC-CN", "csGB2312", "euccn", "euc-cn", "gb2312-80", "gb2312-1980", "CN-GB", "CN-GB-ISOIR165"}), + new Charset("GBK", "windows-936", new String[] {"CP936", "MS936", "ms_936", "x-mswin-936"}), + + new Charset("Cp037", "IBM037", new String[] {"ebcdic-cp-us", "ebcdic-cp-ca", "ebcdic-cp-wt", "ebcdic-cp-nl", "csIBM037"}), + new Charset("Cp273", "IBM273", new String[] {"csIBM273"}), + new Charset("Cp277", "IBM277", new String[] {"EBCDIC-CP-DK", "EBCDIC-CP-NO", "csIBM277"}), + new Charset("Cp278", "IBM278", new String[] {"CP278", "ebcdic-cp-fi", "ebcdic-cp-se", "csIBM278"}), + new Charset("Cp280", "IBM280", new String[] {"ebcdic-cp-it", "csIBM280"}), + new Charset("Cp284", "IBM284", new String[] {"ebcdic-cp-es", "csIBM284"}), + new Charset("Cp285", "IBM285", new String[] {"ebcdic-cp-gb", "csIBM285"}), + new Charset("Cp297", "IBM297", new String[] {"ebcdic-cp-fr", "csIBM297"}), + new Charset("Cp420", "IBM420", new String[] {"ebcdic-cp-ar1", "csIBM420"}), + new Charset("Cp424", "IBM424", new String[] {"ebcdic-cp-he", "csIBM424"}), + new Charset("Cp437", "IBM437", new String[] {"437", "csPC8CodePage437"}), + new Charset("Cp500", "IBM500", new String[] {"ebcdic-cp-be", "ebcdic-cp-ch", "csIBM500"}), + new Charset("Cp775", "IBM775", new String[] {"csPC775Baltic"}), + new Charset("Cp838", "IBM-Thai", new String[] {}), + new Charset("Cp850", "IBM850", new String[] {"850", "csPC850Multilingual"}), + new Charset("Cp852", "IBM852", new String[] {"852", "csPCp852"}), + new Charset("Cp855", "IBM855", new String[] {"855", "csIBM855"}), + new Charset("Cp857", "IBM857", new String[] {"857", "csIBM857"}), + new Charset("Cp858", "IBM00858", + new String[] {"CCSID00858", "CP00858", + "PC-Multilingual-850+euro"}), + new Charset("Cp860", "IBM860", new String[] {"860", "csIBM860"}), + new Charset("Cp861", "IBM861", new String[] {"861", "cp-is", "csIBM861"}), + new Charset("Cp862", "IBM862", new String[] {"862", "csPC862LatinHebrew"}), + new Charset("Cp863", "IBM863", new String[] {"863", "csIBM863"}), + new Charset("Cp864", "IBM864", new String[] {"cp864", "csIBM864"}), + new Charset("Cp865", "IBM865", new String[] {"865", "csIBM865"}), + new Charset("Cp866", "IBM866", new String[] {"866", "csIBM866"}), + new Charset("Cp868", "IBM868", new String[] {"cp-ar", "csIBM868"}), + new Charset("Cp869", "IBM869", new String[] {"cp-gr", "csIBM869"}), + new Charset("Cp870", "IBM870", new String[] {"ebcdic-cp-roece", "ebcdic-cp-yu", "csIBM870"}), + new Charset("Cp871", "IBM871", new String[] {"ebcdic-cp-is", "csIBM871"}), + new Charset("Cp918", "IBM918", new String[] {"ebcdic-cp-ar2", "csIBM918"}), + new Charset("Cp1026", "IBM1026", new String[] {"csIBM1026"}), + new Charset("Cp1047", "IBM1047", new String[] {"IBM-1047"}), + new Charset("Cp1140", "IBM01140", + new String[] {"CCSID01140", "CP01140", + "ebcdic-us-37+euro"}), + new Charset("Cp1141", "IBM01141", + new String[] {"CCSID01141", "CP01141", + "ebcdic-de-273+euro"}), + new Charset("Cp1142", "IBM01142", new String[] {"CCSID01142", "CP01142", "ebcdic-dk-277+euro", "ebcdic-no-277+euro"}), + new Charset("Cp1143", "IBM01143", new String[] {"CCSID01143", "CP01143", "ebcdic-fi-278+euro", "ebcdic-se-278+euro"}), + new Charset("Cp1144", "IBM01144", new String[] {"CCSID01144", "CP01144", "ebcdic-it-280+euro"}), + new Charset("Cp1145", "IBM01145", new String[] {"CCSID01145", "CP01145", "ebcdic-es-284+euro"}), + new Charset("Cp1146", "IBM01146", new String[] {"CCSID01146", "CP01146", "ebcdic-gb-285+euro"}), + new Charset("Cp1147", "IBM01147", new String[] {"CCSID01147", "CP01147", "ebcdic-fr-297+euro"}), + new Charset("Cp1148", "IBM01148", new String[] {"CCSID01148", "CP01148", "ebcdic-international-500+euro"}), + new Charset("Cp1149", "IBM01149", new String[] {"CCSID01149", "CP01149", "ebcdic-is-871+euro"}), + new Charset("Cp1250", "windows-1250", new String[] {}), + new Charset("Cp1251", "windows-1251", new String[] {}), + new Charset("Cp1252", "windows-1252", new String[] {}), + new Charset("Cp1253", "windows-1253", new String[] {}), + new Charset("Cp1254", "windows-1254", new String[] {}), + new Charset("Cp1255", "windows-1255", new String[] {}), + new Charset("Cp1256", "windows-1256", new String[] {}), + new Charset("Cp1257", "windows-1257", new String[] {}), + new Charset("Cp1258", "windows-1258", new String[] {}), + new Charset("ISO2022CN", "ISO-2022-CN", new String[] {}), + new Charset("ISO2022JP", "ISO-2022-JP", new String[] {"csISO2022JP", "JIS", "jis_encoding", "csjisencoding"}), + new Charset("ISO2022KR", "ISO-2022-KR", new String[] {"csISO2022KR"}), + new Charset("JIS_X0201", "JIS_X0201", new String[] {"X0201", "JIS0201", "csHalfWidthKatakana"}), + new Charset("JIS_X0212-1990", "JIS_X0212-1990", new String[] {"iso-ir-159", "x0212", "JIS0212", "csISO159JISX02121990"}), + new Charset("JIS_C6626-1983", "JIS_C6626-1983", new String[] {"x-JIS0208", "JIS0208", "csISO87JISX0208", "x0208", "JIS_X0208-1983", "iso-ir-87"}), + new Charset("SJIS", "Shift_JIS", new String[] {"MS_Kanji", "csShiftJIS", "shift-jis", "x-sjis", "pck"}), + new Charset("TIS620", "TIS-620", new String[] {}), + new Charset("MS932", "Windows-31J", new String[] {"windows-932", "csWindows31J", "x-ms-cp932"}), + new Charset("EUC_TW", "EUC-TW", new String[] {"x-EUC-TW", "cns11643", "euctw"}), + new Charset("x-Johab", "johab", new String[] {"johab", "cp1361", "ms1361", "ksc5601-1992", "ksc5601_1992"}), + new Charset("MS950_HKSCS", "", new String[] {}), + new Charset("MS874", "windows-874", new String[] {"cp874"}), + new Charset("MS949", "windows-949", new String[] {"windows949", "ms_949", "x-windows-949"}), + new Charset("MS950", "windows-950", new String[] {"x-windows-950"}), + + new Charset("Cp737", null, new String[] {}), + new Charset("Cp856", null, new String[] {}), + new Charset("Cp875", null, new String[] {}), + new Charset("Cp921", null, new String[] {}), + new Charset("Cp922", null, new String[] {}), + new Charset("Cp930", null, new String[] {}), + new Charset("Cp933", null, new String[] {}), + new Charset("Cp935", null, new String[] {}), + new Charset("Cp937", null, new String[] {}), + new Charset("Cp939", null, new String[] {}), + new Charset("Cp942", null, new String[] {}), + new Charset("Cp942C", null, new String[] {}), + new Charset("Cp943", null, new String[] {}), + new Charset("Cp943C", null, new String[] {}), + new Charset("Cp948", null, new String[] {}), + new Charset("Cp949", null, new String[] {}), + new Charset("Cp949C", null, new String[] {}), + new Charset("Cp950", null, new String[] {}), + new Charset("Cp964", null, new String[] {}), + new Charset("Cp970", null, new String[] {}), + new Charset("Cp1006", null, new String[] {}), + new Charset("Cp1025", null, new String[] {}), + new Charset("Cp1046", null, new String[] {}), + new Charset("Cp1097", null, new String[] {}), + new Charset("Cp1098", null, new String[] {}), + new Charset("Cp1112", null, new String[] {}), + new Charset("Cp1122", null, new String[] {}), + new Charset("Cp1123", null, new String[] {}), + new Charset("Cp1124", null, new String[] {}), + new Charset("Cp1381", null, new String[] {}), + new Charset("Cp1383", null, new String[] {}), + new Charset("Cp33722", null, new String[] {}), + new Charset("Big5_Solaris", null, new String[] {}), + new Charset("EUC_JP_LINUX", null, new String[] {}), + new Charset("EUC_JP_Solaris", null, new String[] {}), + new Charset("ISCII91", null, new String[] {"x-ISCII91", "iscii"}), + new Charset("ISO2022_CN_CNS", null, new String[] {}), + new Charset("ISO2022_CN_GB", null, new String[] {}), + new Charset("x-iso-8859-11", null, new String[] {}), + new Charset("JISAutoDetect", null, new String[] {}), + new Charset("MacArabic", null, new String[] {}), + new Charset("MacCentralEurope", null, new String[] {}), + new Charset("MacCroatian", null, new String[] {}), + new Charset("MacCyrillic", null, new String[] {}), + new Charset("MacDingbat", null, new String[] {}), + new Charset("MacGreek", "MacGreek", new String[] {}), + new Charset("MacHebrew", null, new String[] {}), + new Charset("MacIceland", null, new String[] {}), + new Charset("MacRoman", "MacRoman", new String[] {"Macintosh", "MAC", "csMacintosh"}), + new Charset("MacRomania", null, new String[] {}), + new Charset("MacSymbol", null, new String[] {}), + new Charset("MacThai", null, new String[] {}), + new Charset("MacTurkish", null, new String[] {}), + new Charset("MacUkraine", null, new String[] {}), + new Charset("UnicodeBig", null, new String[] {}), + new Charset("UnicodeLittle", null, new String[] {}) + }; + + /** + * Contains the canonical names of character sets which can be used to + * decode bytes into Java chars. + */ + private static TreeSet<String> decodingSupported = null; + + /** + * Contains the canonical names of character sets which can be used to + * encode Java chars into bytes. + */ + private static TreeSet<String> encodingSupported = null; + + /** + * Maps character set names to Charset objects. All possible names of + * a charset will be mapped to the Charset. + */ + private static HashMap<String, Charset> charsetMap = null; + + static { + decodingSupported = new TreeSet<String>(); + encodingSupported = new TreeSet<String>(); + byte[] dummy = new byte[] {'d', 'u', 'm', 'm', 'y'}; + for (int i = 0; i < JAVA_CHARSETS.length; i++) { + try { + String s = new String(dummy, JAVA_CHARSETS[i].canonical); + decodingSupported.add(JAVA_CHARSETS[i].canonical.toLowerCase(Locale.US)); + } catch (UnsupportedOperationException e) { + } catch (UnsupportedEncodingException e) { + } + try { + "dummy".getBytes(JAVA_CHARSETS[i].canonical); + encodingSupported.add(JAVA_CHARSETS[i].canonical.toLowerCase(Locale.US)); + } catch (UnsupportedOperationException e) { + } catch (UnsupportedEncodingException e) { + } + } + + charsetMap = new HashMap<String, Charset>(); + for (int i = 0; i < JAVA_CHARSETS.length; i++) { + Charset c = JAVA_CHARSETS[i]; + charsetMap.put(c.canonical.toLowerCase(Locale.US), c); + if (c.mime != null) { + charsetMap.put(c.mime.toLowerCase(Locale.US), c); + } + if (c.aliases != null) { + for (int j = 0; j < c.aliases.length; j++) { + charsetMap.put(c.aliases[j].toLowerCase(Locale.US), c); + } + } + } + + if (log.isDebugEnabled()) { + log.debug("Character sets which support decoding: " + + decodingSupported); + log.debug("Character sets which support encoding: " + + encodingSupported); + } + } + + /** + * ANDROID: THE FOLLOWING SET OF STATIC STRINGS ARE COPIED FROM A NEWER VERSION OF MIME4J + */ + + /** carriage return - line feed sequence */ + public static final String CRLF = "\r\n"; + + /** US-ASCII CR, carriage return (13) */ + public static final int CR = '\r'; + + /** US-ASCII LF, line feed (10) */ + public static final int LF = '\n'; + + /** US-ASCII SP, space (32) */ + public static final int SP = ' '; + + /** US-ASCII HT, horizontal-tab (9)*/ + public static final int HT = '\t'; + + public static final java.nio.charset.Charset US_ASCII = java.nio.charset.Charset + .forName("US-ASCII"); + + public static final java.nio.charset.Charset ISO_8859_1 = java.nio.charset.Charset + .forName("ISO-8859-1"); + + public static final java.nio.charset.Charset UTF_8 = java.nio.charset.Charset + .forName("UTF-8"); + + /** + * Returns <code>true</code> if the specified character is a whitespace + * character (CR, LF, SP or HT). + * + * ANDROID: COPIED FROM A NEWER VERSION OF MIME4J + * + * @param ch + * character to test. + * @return <code>true</code> if the specified character is a whitespace + * character, <code>false</code> otherwise. + */ + public static boolean isWhitespace(char ch) { + return ch == SP || ch == HT || ch == CR || ch == LF; + } + + /** + * Returns <code>true</code> if the specified string consists entirely of + * whitespace characters. + * + * ANDROID: COPIED FROM A NEWER VERSION OF MIME4J + * + * @param s + * string to test. + * @return <code>true</code> if the specified string consists entirely of + * whitespace characters, <code>false</code> otherwise. + */ + public static boolean isWhitespace(final String s) { + if (s == null) { + throw new IllegalArgumentException("String may not be null"); + } + final int len = s.length(); + for (int i = 0; i < len; i++) { + if (!isWhitespace(s.charAt(i))) { + return false; + } + } + return true; + } + + /** + * Determines if the VM supports encoding (chars to bytes) the + * specified character set. NOTE: the given character set name may + * not be known to the VM even if this method returns <code>true</code>. + * Use {@link #toJavaCharset(String)} to get the canonical Java character + * set name. + * + * @param charsetName the characters set name. + * @return <code>true</code> if encoding is supported, <code>false</code> + * otherwise. + */ + public static boolean isEncodingSupported(String charsetName) { + return encodingSupported.contains(charsetName.toLowerCase(Locale.US)); + } + + /** + * Determines if the VM supports decoding (bytes to chars) the + * specified character set. NOTE: the given character set name may + * not be known to the VM even if this method returns <code>true</code>. + * Use {@link #toJavaCharset(String)} to get the canonical Java character + * set name. + * + * @param charsetName the characters set name. + * @return <code>true</code> if decoding is supported, <code>false</code> + * otherwise. + */ + public static boolean isDecodingSupported(String charsetName) { + return decodingSupported.contains(charsetName.toLowerCase(Locale.US)); + } + + /** + * Gets the preferred MIME character set name for the specified + * character set or <code>null</code> if not known. + * + * @param charsetName the character set name to look for. + * @return the MIME preferred name or <code>null</code> if not known. + */ + public static String toMimeCharset(String charsetName) { + Charset c = charsetMap.get(charsetName.toLowerCase(Locale.US)); + if (c != null) { + return c.mime; + } + return null; + } + + /** + * Gets the canonical Java character set name for the specified + * character set or <code>null</code> if not known. This should be + * called before doing any conversions using the Java API. NOTE: + * you must use {@link #isEncodingSupported(String)} or + * {@link #isDecodingSupported(String)} to make sure the returned + * Java character set is supported by the current VM. + * + * @param charsetName the character set name to look for. + * @return the canonical Java name or <code>null</code> if not known. + */ + public static String toJavaCharset(String charsetName) { + Charset c = charsetMap.get(charsetName.toLowerCase(Locale.US)); + if (c != null) { + return c.canonical; + } + return null; + } + + public static java.nio.charset.Charset getCharset(String charsetName) { + String defaultCharset = "ISO-8859-1"; + + // Use the default chareset if given charset is null + if(charsetName == null) charsetName = defaultCharset; + + try { + return java.nio.charset.Charset.forName(charsetName); + } catch (IllegalCharsetNameException e) { + log.info("Illegal charset " + charsetName + ", fallback to " + + defaultCharset + ": " + e); + // Use default charset on exception + return java.nio.charset.Charset.forName(defaultCharset); + } catch (UnsupportedCharsetException ex) { + log.info("Unsupported charset " + charsetName + ", fallback to " + + defaultCharset + ": " + ex); + // Use default charset on exception + return java.nio.charset.Charset.forName(defaultCharset); + } + + } + /* + * Uncomment the code below and run the main method to regenerate the + * Javadoc table above when the known charsets change. + */ + + /* + private static String dumpHtmlTable() { + LinkedList l = new LinkedList(Arrays.asList(JAVA_CHARSETS)); + Collections.sort(l); + StringBuffer sb = new StringBuffer(); + sb.append(" * <table>\n"); + sb.append(" * <tr>\n"); + sb.append(" * <td>Canonical (Java) name</td>\n"); + sb.append(" * <td>MIME preferred</td>\n"); + sb.append(" * <td>Aliases</td>\n"); + sb.append(" * </tr>\n"); + + for (Iterator it = l.iterator(); it.hasNext();) { + Charset c = (Charset) it.next(); + sb.append(" * <tr>\n"); + sb.append(" * <td>" + c.canonical + "</td>\n"); + sb.append(" * <td>" + (c.mime == null ? "?" : c.mime)+ "</td>\n"); + sb.append(" * <td>"); + for (int i = 0; c.aliases != null && i < c.aliases.length; i++) { + sb.append(c.aliases[i] + " "); + } + sb.append("</td>\n"); + sb.append(" * </tr>\n"); + } + sb.append(" * </table>\n"); + return sb.toString(); + } + + public static void main(String[] args) { + System.out.println(dumpHtmlTable()); + }*/ +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java b/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java new file mode 100644 index 000000000..ad3c025cf --- /dev/null +++ b/java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.sync; + +import android.content.Context; +import android.telecom.PhoneAccountHandle; +import android.telephony.PhoneStateListener; +import android.telephony.TelephonyManager; + +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmPhoneStateListener; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * A singleton class designed to remember the active OMTP visual voicemail sources. Because a + * voicemail source is tied 1:1 to a phone account, the phone account handle is used as the key + * for each voicemail source and the associated data. + */ +public class OmtpVvmSourceManager { + public static final String TAG = "OmtpVvmSourceManager"; + + private static OmtpVvmSourceManager sInstance = new OmtpVvmSourceManager(); + + private Context mContext; + private TelephonyManager mTelephonyManager; + // Each phone account is associated with a phone state listener for updates to whether the + // device is able to sync. + private Set<PhoneAccountHandle> mActiveVvmSources; + private Map<PhoneAccountHandle, PhoneStateListener> mPhoneStateListenerMap; + + /** + * Private constructor. Instance should only be acquired through getInstance(). + */ + private OmtpVvmSourceManager() {} + + public static OmtpVvmSourceManager getInstance(Context context) { + sInstance.setup(context); + return sInstance; + } + + /** + * Set the context and system services so they do not need to be retrieved every time. + * @param context The context to get the subscription and telephony manager for. + */ + private void setup(Context context) { + if (mContext == null) { + mContext = context; + mTelephonyManager = (TelephonyManager) + mContext.getSystemService(Context.TELEPHONY_SERVICE); + mActiveVvmSources = Collections.newSetFromMap( + new ConcurrentHashMap<PhoneAccountHandle, Boolean>(8, 0.9f, 1)); + mPhoneStateListenerMap = + new ConcurrentHashMap<PhoneAccountHandle, PhoneStateListener>(8, 0.9f, 1); + } + } + + public void addSource(PhoneAccountHandle phoneAccount) { + mActiveVvmSources.add(phoneAccount); + } + + public void removeSource(PhoneAccountHandle phoneAccount) { + // TODO: should use OmtpVvmCarrierConfigHelper to handle the event. But currently it + // couldn't handle events on removed SIMs + VoicemailStatus.disable(mContext, phoneAccount); + removePhoneStateListener(phoneAccount); + mActiveVvmSources.remove(phoneAccount); + } + + public void addPhoneStateListener(PhoneAccountHandle phoneAccount) { + if (!mPhoneStateListenerMap.containsKey(phoneAccount)) { + VvmPhoneStateListener phoneStateListener = new VvmPhoneStateListener(mContext, + phoneAccount); + mPhoneStateListenerMap.put(phoneAccount, phoneStateListener); + mTelephonyManager.createForPhoneAccountHandle(phoneAccount) + .listen(phoneStateListener, PhoneStateListener.LISTEN_SERVICE_STATE); + } + } + + public void removePhoneStateListener(PhoneAccountHandle phoneAccount) { + PhoneStateListener phoneStateListener = + mPhoneStateListenerMap.remove(phoneAccount); + mTelephonyManager.createForPhoneAccountHandle(phoneAccount).listen(phoneStateListener, 0); + } + + public Set<PhoneAccountHandle> getOmtpVvmSources() { + return mActiveVvmSources; + } + + /** + * Check if a certain account is registered. + * + * @param phoneAccount The account to look for. + * @return {@code true} if the account is in the list of registered OMTP voicemail sources. + * {@code false} otherwise. + */ + public boolean isVvmSourceRegistered(PhoneAccountHandle phoneAccount) { + if (phoneAccount == null) { + return false; + } + + return mActiveVvmSources.contains(phoneAccount); + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java b/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java new file mode 100644 index 000000000..971a1c5a8 --- /dev/null +++ b/java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.sync; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.provider.VoicemailContract; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; + +import com.android.voicemailomtp.ActivationTask; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil; + +import java.util.List; + +public class OmtpVvmSyncReceiver extends BroadcastReceiver { + + private static final String TAG = "OmtpVvmSyncReceiver"; + + @Override + public void onReceive(final Context context, Intent intent) { + if (VoicemailContract.ACTION_SYNC_VOICEMAIL.equals(intent.getAction())) { + VvmLog.v(TAG, "Sync intent received"); + for (PhoneAccountHandle source : OmtpVvmSourceManager.getInstance(context) + .getOmtpVvmSources()) { + SyncTask.start(context, source, OmtpVvmSyncService.SYNC_FULL_SYNC); + } + activateUnactivatedAccounts(context); + } + } + + private static void activateUnactivatedAccounts(Context context) { + List<PhoneAccountHandle> accounts = + context.getSystemService(TelecomManager.class).getCallCapablePhoneAccounts(); + for (PhoneAccountHandle phoneAccount : accounts) { + if (!VisualVoicemailSettingsUtil.isEnabled(context, phoneAccount)) { + continue; + } + if (!OmtpVvmSourceManager.getInstance(context).isVvmSourceRegistered(phoneAccount)) { + VvmLog.i(TAG, "Unactivated account " + phoneAccount + " found, activating"); + ActivationTask.start(context, phoneAccount, null); + } + } + } +} diff --git a/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java b/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java new file mode 100644 index 000000000..a3418cc28 --- /dev/null +++ b/java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java @@ -0,0 +1,278 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.sync; + +import android.annotation.TargetApi; +import android.content.Context; +import android.net.Network; +import android.net.Uri; +import android.os.Build.VERSION_CODES; +import android.telecom.PhoneAccountHandle; +import android.text.TextUtils; +import com.android.voicemailomtp.ActivationTask; +import com.android.voicemailomtp.Assert; +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.Voicemail; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.fetch.VoicemailFetchedCallback; +import com.android.voicemailomtp.imap.ImapHelper; +import com.android.voicemailomtp.imap.ImapHelper.InitializingException; +import com.android.voicemailomtp.scheduling.BaseTask; +import com.android.voicemailomtp.settings.VisualVoicemailSettingsUtil; +import com.android.voicemailomtp.sync.VvmNetworkRequest.NetworkWrapper; +import com.android.voicemailomtp.sync.VvmNetworkRequest.RequestFailedException; +import com.android.voicemailomtp.utils.VoicemailDatabaseUtil; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** Sync OMTP visual voicemail. */ +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class OmtpVvmSyncService { + + private static final String TAG = OmtpVvmSyncService.class.getSimpleName(); + + /** + * Signifies a sync with both uploading to the server and downloading from the server. + */ + public static final String SYNC_FULL_SYNC = "full_sync"; + /** + * Only upload to the server. + */ + public static final String SYNC_UPLOAD_ONLY = "upload_only"; + /** + * Only download from the server. + */ + public static final String SYNC_DOWNLOAD_ONLY = "download_only"; + /** + * Only download single voicemail transcription. + */ + public static final String SYNC_DOWNLOAD_ONE_TRANSCRIPTION = + "download_one_transcription"; + + private final Context mContext; + + // Record the timestamp of the last full sync so that duplicate syncs can be reduced. + private static final String LAST_FULL_SYNC_TIMESTAMP = "last_full_sync_timestamp"; + // Constant indicating that there has never been a full sync. + public static final long NO_PRIOR_FULL_SYNC = -1; + + private VoicemailsQueryHelper mQueryHelper; + + public OmtpVvmSyncService(Context context) { + mContext = context; + mQueryHelper = new VoicemailsQueryHelper(mContext); + } + + public void sync(BaseTask task, String action, PhoneAccountHandle phoneAccount, + Voicemail voicemail, VoicemailStatus.Editor status) { + Assert.isTrue(phoneAccount != null); + VvmLog.v(TAG, "Sync requested: " + action + " - for account: " + phoneAccount); + setupAndSendRequest(task, phoneAccount, voicemail, action, status); + } + + private void setupAndSendRequest(BaseTask task, PhoneAccountHandle phoneAccount, + Voicemail voicemail, String action, VoicemailStatus.Editor status) { + if (!VisualVoicemailSettingsUtil.isEnabled(mContext, phoneAccount)) { + VvmLog.v(TAG, "Sync requested for disabled account"); + return; + } + if (!OmtpVvmSourceManager.getInstance(mContext).isVvmSourceRegistered(phoneAccount)) { + ActivationTask.start(mContext, phoneAccount, null); + return; + } + + OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(mContext, phoneAccount); + // DATA_IMAP_OPERATION_STARTED posting should not be deferred. This event clears all data + // channel errors, which should happen when the task starts, not when it ends. It is the + // "Sync in progress..." status. + config.handleEvent(VoicemailStatus.edit(mContext, phoneAccount), + OmtpEvents.DATA_IMAP_OPERATION_STARTED); + try (NetworkWrapper network = VvmNetworkRequest.getNetwork(config, phoneAccount, status)) { + if (network == null) { + VvmLog.e(TAG, "unable to acquire network"); + task.fail(); + return; + } + doSync(task, network.get(), phoneAccount, voicemail, action, status); + } catch (RequestFailedException e) { + config.handleEvent(status, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED); + task.fail(); + } + } + + private void doSync(BaseTask task, Network network, PhoneAccountHandle phoneAccount, + Voicemail voicemail, String action, VoicemailStatus.Editor status) { + try (ImapHelper imapHelper = new ImapHelper(mContext, phoneAccount, network, status)) { + boolean success; + if (voicemail == null) { + success = syncAll(action, imapHelper, phoneAccount); + } else { + success = syncOne(imapHelper, voicemail, phoneAccount); + } + if (success) { + // TODO: b/30569269 failure should interrupt all subsequent task via exceptions + imapHelper.updateQuota(); + imapHelper.handleEvent(OmtpEvents.DATA_IMAP_OPERATION_COMPLETED); + } else { + task.fail(); + } + } catch (InitializingException e) { + VvmLog.w(TAG, "Can't retrieve Imap credentials.", e); + return; + } + } + + private boolean syncAll(String action, ImapHelper imapHelper, PhoneAccountHandle account) { + boolean uploadSuccess = true; + boolean downloadSuccess = true; + + if (SYNC_FULL_SYNC.equals(action) || SYNC_UPLOAD_ONLY.equals(action)) { + uploadSuccess = upload(imapHelper); + } + if (SYNC_FULL_SYNC.equals(action) || SYNC_DOWNLOAD_ONLY.equals(action)) { + downloadSuccess = download(imapHelper, account); + } + + VvmLog.v(TAG, "upload succeeded: [" + String.valueOf(uploadSuccess) + + "] download succeeded: [" + String.valueOf(downloadSuccess) + "]"); + + return uploadSuccess && downloadSuccess; + } + + private boolean syncOne(ImapHelper imapHelper, Voicemail voicemail, + PhoneAccountHandle account) { + if (shouldPerformPrefetch(account, imapHelper)) { + VoicemailFetchedCallback callback = new VoicemailFetchedCallback(mContext, + voicemail.getUri(), account); + imapHelper.fetchVoicemailPayload(callback, voicemail.getSourceData()); + } + + return imapHelper.fetchTranscription( + new TranscriptionFetchedCallback(mContext, voicemail), + voicemail.getSourceData()); + } + + private boolean upload(ImapHelper imapHelper) { + List<Voicemail> readVoicemails = mQueryHelper.getReadVoicemails(); + List<Voicemail> deletedVoicemails = mQueryHelper.getDeletedVoicemails(); + + boolean success = true; + + if (deletedVoicemails.size() > 0) { + if (imapHelper.markMessagesAsDeleted(deletedVoicemails)) { + // We want to delete selectively instead of all the voicemails for this provider + // in case the state changed since the IMAP query was completed. + mQueryHelper.deleteFromDatabase(deletedVoicemails); + } else { + success = false; + } + } + + if (readVoicemails.size() > 0) { + if (imapHelper.markMessagesAsRead(readVoicemails)) { + mQueryHelper.markCleanInDatabase(readVoicemails); + } else { + success = false; + } + } + + return success; + } + + private boolean download(ImapHelper imapHelper, PhoneAccountHandle account) { + List<Voicemail> serverVoicemails = imapHelper.fetchAllVoicemails(); + List<Voicemail> localVoicemails = mQueryHelper.getAllVoicemails(); + + if (localVoicemails == null || serverVoicemails == null) { + // Null value means the query failed. + return false; + } + + Map<String, Voicemail> remoteMap = buildMap(serverVoicemails); + + // Go through all the local voicemails and check if they are on the server. + // They may be read or deleted on the server but not locally. Perform the + // appropriate local operation if the status differs from the server. Remove + // the messages that exist both locally and on the server to know which server + // messages to insert locally. + for (int i = 0; i < localVoicemails.size(); i++) { + Voicemail localVoicemail = localVoicemails.get(i); + Voicemail remoteVoicemail = remoteMap.remove(localVoicemail.getSourceData()); + if (remoteVoicemail == null) { + mQueryHelper.deleteFromDatabase(localVoicemail); + } else { + if (remoteVoicemail.isRead() != localVoicemail.isRead()) { + mQueryHelper.markReadInDatabase(localVoicemail); + } + + if (!TextUtils.isEmpty(remoteVoicemail.getTranscription()) && + TextUtils.isEmpty(localVoicemail.getTranscription())) { + mQueryHelper.updateWithTranscription(localVoicemail, + remoteVoicemail.getTranscription()); + } + } + } + + // The leftover messages are messages that exist on the server but not locally. + boolean prefetchEnabled = shouldPerformPrefetch(account, imapHelper); + for (Voicemail remoteVoicemail : remoteMap.values()) { + Uri uri = VoicemailDatabaseUtil.insert(mContext, remoteVoicemail); + if (prefetchEnabled) { + VoicemailFetchedCallback fetchedCallback = + new VoicemailFetchedCallback(mContext, uri, account); + imapHelper.fetchVoicemailPayload(fetchedCallback, remoteVoicemail.getSourceData()); + } + } + + return true; + } + + private boolean shouldPerformPrefetch(PhoneAccountHandle account, ImapHelper imapHelper) { + OmtpVvmCarrierConfigHelper carrierConfigHelper = + new OmtpVvmCarrierConfigHelper(mContext, account); + return carrierConfigHelper.isPrefetchEnabled() && !imapHelper.isRoaming(); + } + + /** + * Builds a map from provider data to message for the given collection of voicemails. + */ + private Map<String, Voicemail> buildMap(List<Voicemail> messages) { + Map<String, Voicemail> map = new HashMap<String, Voicemail>(); + for (Voicemail message : messages) { + map.put(message.getSourceData(), message); + } + return map; + } + + public class TranscriptionFetchedCallback { + + private Context mContext; + private Voicemail mVoicemail; + + public TranscriptionFetchedCallback(Context context, Voicemail voicemail) { + mContext = context; + mVoicemail = voicemail; + } + + public void setVoicemailTranscription(String transcription) { + VoicemailsQueryHelper queryHelper = new VoicemailsQueryHelper(mContext); + queryHelper.updateWithTranscription(mVoicemail, transcription); + } + } +} diff --git a/java/com/android/voicemailomtp/sync/SyncOneTask.java b/java/com/android/voicemailomtp/sync/SyncOneTask.java new file mode 100644 index 000000000..9264e6c08 --- /dev/null +++ b/java/com/android/voicemailomtp/sync/SyncOneTask.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.sync; + +import android.content.Context; +import android.content.Intent; +import android.telecom.PhoneAccountHandle; + +import com.android.voicemailomtp.Voicemail; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.scheduling.BaseTask; +import com.android.voicemailomtp.scheduling.RetryPolicy; + +/** + * Task to download a single voicemail from the server. This task is initiated by a SMS notifying + * the new voicemail arrival, and ignores the duplicated tasks constraint. + */ +public class SyncOneTask extends BaseTask { + + private static final int RETRY_TIMES = 2; + private static final int RETRY_INTERVAL_MILLIS = 5_000; + + private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle"; + private static final String EXTRA_SYNC_TYPE = "extra_sync_type"; + private static final String EXTRA_VOICEMAIL = "extra_voicemail"; + + private PhoneAccountHandle mPhone; + private String mSyncType; + private Voicemail mVoicemail; + + public static void start(Context context, PhoneAccountHandle phone, Voicemail voicemail) { + Intent intent = BaseTask + .createIntent(context, SyncOneTask.class, phone); + intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone); + intent.putExtra(EXTRA_SYNC_TYPE, OmtpVvmSyncService.SYNC_DOWNLOAD_ONE_TRANSCRIPTION); + intent.putExtra(EXTRA_VOICEMAIL, voicemail); + context.startService(intent); + } + + public SyncOneTask() { + super(TASK_ALLOW_DUPLICATES); + addPolicy(new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS)); + } + + public void onCreate(Context context, Intent intent, int flags, int startId) { + super.onCreate(context, intent, flags, startId); + mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE); + mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE); + mVoicemail = intent.getParcelableExtra(EXTRA_VOICEMAIL); + } + + @Override + public void onExecuteInBackgroundThread() { + OmtpVvmSyncService service = new OmtpVvmSyncService(getContext()); + service.sync(this, mSyncType, mPhone, mVoicemail, + VoicemailStatus.edit(getContext(), mPhone)); + } + + @Override + public Intent createRestartIntent() { + Intent intent = super.createRestartIntent(); + intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone); + intent.putExtra(EXTRA_SYNC_TYPE, mSyncType); + intent.putExtra(EXTRA_VOICEMAIL, mVoicemail); + return intent; + } + +} diff --git a/java/com/android/voicemailomtp/sync/SyncTask.java b/java/com/android/voicemailomtp/sync/SyncTask.java new file mode 100644 index 000000000..41b22f22c --- /dev/null +++ b/java/com/android/voicemailomtp/sync/SyncTask.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.sync; + +import android.content.Context; +import android.content.Intent; +import android.telecom.PhoneAccountHandle; + +import com.android.voicemailomtp.scheduling.BaseTask; +import com.android.voicemailomtp.scheduling.MinimalIntervalPolicy; +import com.android.voicemailomtp.scheduling.RetryPolicy; + +/** + * System initiated sync request. + */ +public class SyncTask extends BaseTask { + + // Try sync for a total of 5 times, should take around 5 minutes before finally giving up. + private static final int RETRY_TIMES = 4; + private static final int RETRY_INTERVAL_MILLIS = 5_000; + private static final int MINIMAL_INTERVAL_MILLIS = 60_000; + + private static final String EXTRA_PHONE_ACCOUNT_HANDLE = "extra_phone_account_handle"; + private static final String EXTRA_SYNC_TYPE = "extra_sync_type"; + + private final RetryPolicy mRetryPolicy; + + private PhoneAccountHandle mPhone; + private String mSyncType; + + public static void start(Context context, PhoneAccountHandle phone, String syncType) { + Intent intent = BaseTask + .createIntent(context, SyncTask.class, phone); + intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, phone); + intent.putExtra(EXTRA_SYNC_TYPE, syncType); + context.startService(intent); + } + + public SyncTask() { + super(TASK_SYNC); + mRetryPolicy = new RetryPolicy(RETRY_TIMES, RETRY_INTERVAL_MILLIS); + addPolicy(mRetryPolicy); + addPolicy(new MinimalIntervalPolicy(MINIMAL_INTERVAL_MILLIS)); + } + + public void onCreate(Context context, Intent intent, int flags, int startId) { + super.onCreate(context, intent, flags, startId); + mPhone = intent.getParcelableExtra(EXTRA_PHONE_ACCOUNT_HANDLE); + mSyncType = intent.getStringExtra(EXTRA_SYNC_TYPE); + } + + @Override + public void onExecuteInBackgroundThread() { + OmtpVvmSyncService service = new OmtpVvmSyncService(getContext()); + service.sync(this, mSyncType, mPhone, null, mRetryPolicy.getVoicemailStatusEditor()); + } + + @Override + public Intent createRestartIntent() { + Intent intent = super.createRestartIntent(); + intent.putExtra(EXTRA_PHONE_ACCOUNT_HANDLE, mPhone); + intent.putExtra(EXTRA_SYNC_TYPE, mSyncType); + return intent; + } +} diff --git a/java/com/android/voicemailomtp/sync/UploadTask.java b/java/com/android/voicemailomtp/sync/UploadTask.java new file mode 100644 index 000000000..30a16812b --- /dev/null +++ b/java/com/android/voicemailomtp/sync/UploadTask.java @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.sync; + +import android.content.Context; +import android.content.Intent; +import android.telecom.PhoneAccountHandle; + +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; +import com.android.voicemailomtp.scheduling.BaseTask; +import com.android.voicemailomtp.scheduling.PostponePolicy; + +/** + * Upload task triggered by database changes. Will wait until the database has been stable for + * {@link #POSTPONE_MILLIS} to execute. + */ +public class UploadTask extends BaseTask { + + private static final String TAG = "VvmUploadTask"; + + private static final int POSTPONE_MILLIS = 5_000; + + public UploadTask() { + super(TASK_UPLOAD); + addPolicy(new PostponePolicy(POSTPONE_MILLIS)); + } + + public static void start(Context context, PhoneAccountHandle phoneAccountHandle) { + Intent intent = BaseTask + .createIntent(context, UploadTask.class, phoneAccountHandle); + context.startService(intent); + } + + @Override + public void onCreate(Context context, Intent intent, int flags, int startId) { + super.onCreate(context, intent, flags, startId); + } + + @Override + public void onExecuteInBackgroundThread() { + OmtpVvmSyncService service = new OmtpVvmSyncService(getContext()); + + PhoneAccountHandle phoneAccountHandle = getPhoneAccountHandle(); + if (phoneAccountHandle == null) { + // This should never happen + VvmLog.e(TAG, "null phone account for phoneAccountHandle " + getPhoneAccountHandle()); + return; + } + service.sync(this, OmtpVvmSyncService.SYNC_UPLOAD_ONLY, + phoneAccountHandle, null, + VoicemailStatus.edit(getContext(), phoneAccountHandle)); + } +} diff --git a/java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java b/java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java new file mode 100644 index 000000000..ade9ef12d --- /dev/null +++ b/java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.sync; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.provider.VoicemailContract; +import android.telecom.PhoneAccountHandle; + +/** + * Receives changes to the voicemail provider so they can be sent to the voicemail server. + */ +public class VoicemailProviderChangeReceiver extends BroadcastReceiver { + + @Override + public void onReceive(Context context, Intent intent) { + boolean isSelfChanged = intent.getBooleanExtra(VoicemailContract.EXTRA_SELF_CHANGE, false); + OmtpVvmSourceManager vvmSourceManager = + OmtpVvmSourceManager.getInstance(context); + if (vvmSourceManager.getOmtpVvmSources().size() > 0 && !isSelfChanged) { + for (PhoneAccountHandle source : OmtpVvmSourceManager.getInstance(context) + .getOmtpVvmSources()) { + UploadTask.start(context, source); + } + } + } +} diff --git a/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java b/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java new file mode 100644 index 000000000..89ba0b494 --- /dev/null +++ b/java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.sync; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.VoicemailContract; +import android.provider.VoicemailContract.Status; +import android.telecom.PhoneAccountHandle; + +/** + * Construct queries to interact with the voicemail status table. + */ +public class VoicemailStatusQueryHelper { + + final static String[] PROJECTION = new String[] { + Status._ID, // 0 + Status.CONFIGURATION_STATE, // 1 + Status.NOTIFICATION_CHANNEL_STATE, // 2 + Status.SOURCE_PACKAGE // 3 + }; + + public static final int _ID = 0; + public static final int CONFIGURATION_STATE = 1; + public static final int NOTIFICATION_CHANNEL_STATE = 2; + public static final int SOURCE_PACKAGE = 3; + + private Context mContext; + private ContentResolver mContentResolver; + private Uri mSourceUri; + + public VoicemailStatusQueryHelper(Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + mSourceUri = VoicemailContract.Status.buildSourceUri(mContext.getPackageName()); + } + + /** + * Check if the configuration state for the voicemail source is "ok", meaning that the + * source is set up. + * + * @param phoneAccount The phone account for the voicemail source to check. + * @return {@code true} if the voicemail source is configured, {@code} false otherwise, + * including if the voicemail source is not registered in the table. + */ + public boolean isVoicemailSourceConfigured(PhoneAccountHandle phoneAccount) { + return isFieldEqualTo(phoneAccount, CONFIGURATION_STATE, Status.CONFIGURATION_STATE_OK); + } + + /** + * Check if the notifications channel of a voicemail source is active. That is, when a new + * voicemail is available, if the server able to notify the device. + * + * @return {@code true} if notifications channel is active, {@code false} otherwise. + */ + public boolean isNotificationsChannelActive(PhoneAccountHandle phoneAccount) { + return isFieldEqualTo(phoneAccount, NOTIFICATION_CHANNEL_STATE, + Status.NOTIFICATION_CHANNEL_STATE_OK); + } + + /** + * Check if a field for an entry in the status table is equal to a specific value. + * + * @param phoneAccount The phone account of the voicemail source to query for. + * @param columnIndex The column index of the field in the returned query. + * @param value The value to compare against. + * @return {@code true} if the stored value is equal to the provided value. {@code false} + * otherwise. + */ + private boolean isFieldEqualTo(PhoneAccountHandle phoneAccount, int columnIndex, int value) { + Cursor cursor = null; + if (phoneAccount != null) { + String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString(); + String phoneAccountId = phoneAccount.getId(); + if (phoneAccountComponentName == null || phoneAccountId == null) { + return false; + } + try { + String whereClause = + Status.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " + + Status.PHONE_ACCOUNT_ID + "=? AND " + Status.SOURCE_PACKAGE + "=?"; + String[] whereArgs = { phoneAccountComponentName, phoneAccountId, + mContext.getPackageName()}; + cursor = mContentResolver.query( + mSourceUri, PROJECTION, whereClause, whereArgs, null); + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(columnIndex) == value; + } + } + finally { + if (cursor != null) { + cursor.close(); + } + } + } + return false; + } +} diff --git a/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java b/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java new file mode 100644 index 000000000..1450e3d1b --- /dev/null +++ b/java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java @@ -0,0 +1,244 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.sync; + +import android.content.ContentResolver; +import android.content.ContentUris; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.VoicemailContract; +import android.provider.VoicemailContract.Voicemails; +import android.telecom.PhoneAccountHandle; +import com.android.voicemailomtp.Voicemail; +import java.util.ArrayList; +import java.util.List; + +/** + * Construct queries to interact with the voicemails table. + */ +public class VoicemailsQueryHelper { + final static String[] PROJECTION = new String[] { + Voicemails._ID, // 0 + Voicemails.SOURCE_DATA, // 1 + Voicemails.IS_READ, // 2 + Voicemails.DELETED, // 3 + Voicemails.TRANSCRIPTION // 4 + }; + + public static final int _ID = 0; + public static final int SOURCE_DATA = 1; + public static final int IS_READ = 2; + public static final int DELETED = 3; + public static final int TRANSCRIPTION = 4; + + final static String READ_SELECTION = Voicemails.DIRTY + "=1 AND " + + Voicemails.DELETED + "!=1 AND " + Voicemails.IS_READ + "=1"; + final static String DELETED_SELECTION = Voicemails.DELETED + "=1"; + + private Context mContext; + private ContentResolver mContentResolver; + private Uri mSourceUri; + + public VoicemailsQueryHelper(Context context) { + mContext = context; + mContentResolver = context.getContentResolver(); + mSourceUri = VoicemailContract.Voicemails.buildSourceUri(mContext.getPackageName()); + } + + /** + * Get all the local read voicemails that have not been synced to the server. + * + * @return A list of read voicemails. + */ + public List<Voicemail> getReadVoicemails() { + return getLocalVoicemails(READ_SELECTION); + } + + /** + * Get all the locally deleted voicemails that have not been synced to the server. + * + * @return A list of deleted voicemails. + */ + public List<Voicemail> getDeletedVoicemails() { + return getLocalVoicemails(DELETED_SELECTION); + } + + /** + * Get all voicemails locally stored. + * + * @return A list of all locally stored voicemails. + */ + public List<Voicemail> getAllVoicemails() { + return getLocalVoicemails(null); + } + + /** + * Utility method to make queries to the voicemail database. + * + * @param selection A filter declaring which rows to return. {@code null} returns all rows. + * @return A list of voicemails according to the selection statement. + */ + private List<Voicemail> getLocalVoicemails(String selection) { + Cursor cursor = mContentResolver.query(mSourceUri, PROJECTION, selection, null, null); + if (cursor == null) { + return null; + } + try { + List<Voicemail> voicemails = new ArrayList<Voicemail>(); + while (cursor.moveToNext()) { + final long id = cursor.getLong(_ID); + final String sourceData = cursor.getString(SOURCE_DATA); + final boolean isRead = cursor.getInt(IS_READ) == 1; + final String transcription = cursor.getString(TRANSCRIPTION); + Voicemail voicemail = Voicemail + .createForUpdate(id, sourceData) + .setIsRead(isRead) + .setTranscription(transcription).build(); + voicemails.add(voicemail); + } + return voicemails; + } finally { + cursor.close(); + } + } + + /** + * Deletes a list of voicemails from the voicemail content provider. + * + * @param voicemails The list of voicemails to delete + * @return The number of voicemails deleted + */ + public int deleteFromDatabase(List<Voicemail> voicemails) { + int count = voicemails.size(); + if (count == 0) { + return 0; + } + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < count; i++) { + if (i > 0) { + sb.append(","); + } + sb.append(voicemails.get(i).getId()); + } + + String selectionStatement = String.format(Voicemails._ID + " IN (%s)", sb.toString()); + return mContentResolver.delete(Voicemails.CONTENT_URI, selectionStatement, null); + } + + /** + * Utility method to delete a single voicemail. + */ + public void deleteFromDatabase(Voicemail voicemail) { + mContentResolver.delete(Voicemails.CONTENT_URI, Voicemails._ID + "=?", + new String[] { Long.toString(voicemail.getId()) }); + } + + public int markReadInDatabase(List<Voicemail> voicemails) { + int count = voicemails.size(); + for (int i = 0; i < count; i++) { + markReadInDatabase(voicemails.get(i)); + } + return count; + } + + /** + * Utility method to mark single message as read. + */ + public void markReadInDatabase(Voicemail voicemail) { + Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId()); + ContentValues contentValues = new ContentValues(); + contentValues.put(Voicemails.IS_READ, "1"); + mContentResolver.update(uri, contentValues, null, null); + } + + /** + * Sends an update command to the voicemail content provider for a list of voicemails. From the + * view of the provider, since the updater is the owner of the entry, a blank "update" means + * that the voicemail source is indicating that the server has up-to-date information on the + * voicemail. This flips the "dirty" bit to "0". + * + * @param voicemails The list of voicemails to update + * @return The number of voicemails updated + */ + public int markCleanInDatabase(List<Voicemail> voicemails) { + int count = voicemails.size(); + for (int i = 0; i < count; i++) { + markCleanInDatabase(voicemails.get(i)); + } + return count; + } + + /** + * Utility method to mark single message as clean. + */ + public void markCleanInDatabase(Voicemail voicemail) { + Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId()); + ContentValues contentValues = new ContentValues(); + mContentResolver.update(uri, contentValues, null, null); + } + + /** + * Utility method to add a transcription to the voicemail. + */ + public void updateWithTranscription(Voicemail voicemail, String transcription) { + Uri uri = ContentUris.withAppendedId(mSourceUri, voicemail.getId()); + ContentValues contentValues = new ContentValues(); + contentValues.put(Voicemails.TRANSCRIPTION, transcription); + mContentResolver.update(uri, contentValues, null, null); + } + + /** + * Voicemail is unique if the tuple of (phone account component name, phone account id, source + * data) is unique. If the phone account is missing, we also consider this unique since it's + * simply an "unknown" account. + * @param voicemail The voicemail to check if it is unique. + * @return {@code true} if the voicemail is unique, {@code false} otherwise. + */ + public boolean isVoicemailUnique(Voicemail voicemail) { + Cursor cursor = null; + PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount(); + if (phoneAccount != null) { + String phoneAccountComponentName = phoneAccount.getComponentName().flattenToString(); + String phoneAccountId = phoneAccount.getId(); + String sourceData = voicemail.getSourceData(); + if (phoneAccountComponentName == null || phoneAccountId == null || sourceData == null) { + return true; + } + try { + String whereClause = + Voicemails.PHONE_ACCOUNT_COMPONENT_NAME + "=? AND " + + Voicemails.PHONE_ACCOUNT_ID + "=? AND " + Voicemails.SOURCE_DATA + "=?"; + String[] whereArgs = { phoneAccountComponentName, phoneAccountId, sourceData }; + cursor = mContentResolver.query( + mSourceUri, PROJECTION, whereClause, whereArgs, null); + if (cursor.getCount() == 0) { + return true; + } else { + return false; + } + } + finally { + if (cursor != null) { + cursor.close(); + } + } + } + return true; + } +} diff --git a/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java b/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java new file mode 100644 index 000000000..966b940c2 --- /dev/null +++ b/java/com/android/voicemailomtp/sync/VvmNetworkRequest.java @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.sync; + +import android.annotation.TargetApi; +import android.net.Network; +import android.os.Build.VERSION_CODES; +import android.support.annotation.NonNull; +import android.telecom.PhoneAccountHandle; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; +import java.io.Closeable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; + +/** + * Class to retrieve a {@link Network} synchronously. {@link #getNetwork(OmtpVvmCarrierConfigHelper, + * PhoneAccountHandle)} will block until a suitable network is retrieved or it has failed. + */ +@SuppressWarnings("AndroidApiChecker") /* CompletableFuture is java8*/ +@TargetApi(VERSION_CODES.CUR_DEVELOPMENT) +public class VvmNetworkRequest { + + private static final String TAG = "VvmNetworkRequest"; + + /** + * A wrapper around a Network returned by a {@link VvmNetworkRequestCallback}, which should be + * closed once not needed anymore. + */ + public static class NetworkWrapper implements Closeable { + + private final Network mNetwork; + private final VvmNetworkRequestCallback mCallback; + + private NetworkWrapper(Network network, VvmNetworkRequestCallback callback) { + mNetwork = network; + mCallback = callback; + } + + public Network get() { + return mNetwork; + } + + @Override + public void close() { + mCallback.releaseNetwork(); + } + } + + public static class RequestFailedException extends Exception { + + private RequestFailedException(Throwable cause) { + super(cause); + } + } + + @NonNull + public static NetworkWrapper getNetwork(OmtpVvmCarrierConfigHelper config, + PhoneAccountHandle handle, VoicemailStatus.Editor status) throws RequestFailedException { + FutureNetworkRequestCallback callback = new FutureNetworkRequestCallback(config, handle, + status); + callback.requestNetwork(); + try { + return callback.getFuture().get(); + } catch (InterruptedException | ExecutionException e) { + callback.releaseNetwork(); + VvmLog.e(TAG, "can't get future network", e); + throw new RequestFailedException(e); + } + } + + private static class FutureNetworkRequestCallback extends VvmNetworkRequestCallback { + + /** + * {@link CompletableFuture#get()} will block until {@link CompletableFuture# + * complete(Object) } has been called on the other thread. + */ + private final CompletableFuture<NetworkWrapper> mFuture = new CompletableFuture<>(); + + public FutureNetworkRequestCallback(OmtpVvmCarrierConfigHelper config, + PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) { + super(config, phoneAccount, status); + } + + public Future<NetworkWrapper> getFuture() { + return mFuture; + } + + @Override + public void onAvailable(Network network) { + super.onAvailable(network); + mFuture.complete(new NetworkWrapper(network, this)); + } + + @Override + public void onFailed(String reason) { + super.onFailed(reason); + mFuture.complete(null); + } + + } +} diff --git a/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java b/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java new file mode 100644 index 000000000..8481a9d16 --- /dev/null +++ b/java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2015 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.android.voicemailomtp.sync; + +import android.content.Context; +import android.net.ConnectivityManager; +import android.net.Network; +import android.net.NetworkCapabilities; +import android.net.NetworkRequest; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.CallSuper; +import android.telecom.PhoneAccountHandle; + +import com.android.voicemailomtp.OmtpEvents; +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.TelephonyManagerStub; +import com.android.voicemailomtp.VoicemailStatus; +import com.android.voicemailomtp.VvmLog; + +/** + * Base class for network request call backs for visual voicemail syncing with the Imap server. This + * handles retries and network requests. + */ +public abstract class VvmNetworkRequestCallback extends ConnectivityManager.NetworkCallback { + + private static final String TAG = "VvmNetworkRequest"; + + // Timeout used to call ConnectivityManager.requestNetwork + private static final int NETWORK_REQUEST_TIMEOUT_MILLIS = 60 * 1000; + + public static final String NETWORK_REQUEST_FAILED_TIMEOUT = "timeout"; + public static final String NETWORK_REQUEST_FAILED_LOST = "lost"; + + protected Context mContext; + protected PhoneAccountHandle mPhoneAccount; + protected NetworkRequest mNetworkRequest; + private ConnectivityManager mConnectivityManager; + private final OmtpVvmCarrierConfigHelper mCarrierConfigHelper; + private final VoicemailStatus.Editor mStatus; + private boolean mRequestSent = false; + private boolean mResultReceived = false; + + public VvmNetworkRequestCallback(Context context, PhoneAccountHandle phoneAccount, + VoicemailStatus.Editor status) { + mContext = context; + mPhoneAccount = phoneAccount; + mStatus = status; + mCarrierConfigHelper = new OmtpVvmCarrierConfigHelper(context, mPhoneAccount); + mNetworkRequest = createNetworkRequest(); + } + + public VvmNetworkRequestCallback(OmtpVvmCarrierConfigHelper config, + PhoneAccountHandle phoneAccount, VoicemailStatus.Editor status) { + mContext = config.getContext(); + mPhoneAccount = phoneAccount; + mStatus = status; + mCarrierConfigHelper = config; + mNetworkRequest = createNetworkRequest(); + } + + public VoicemailStatus.Editor getVoicemailStatusEditor() { + return mStatus; + } + + /** + * @return NetworkRequest for a proper transport type. Use only cellular network if the carrier + * requires it. Otherwise use whatever available. + */ + private NetworkRequest createNetworkRequest() { + + NetworkRequest.Builder builder = new NetworkRequest.Builder() + .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET); + + if (mCarrierConfigHelper.isCellularDataRequired()) { + VvmLog.d(TAG, "Transport type: CELLULAR"); + builder.addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR) + .setNetworkSpecifier(TelephonyManagerStub + .getNetworkSpecifierForPhoneAccountHandle(mContext, mPhoneAccount)); + } else { + VvmLog.d(TAG, "Transport type: ANY"); + } + return builder.build(); + } + + public NetworkRequest getNetworkRequest() { + return mNetworkRequest; + } + + @Override + @CallSuper + public void onLost(Network network) { + VvmLog.d(TAG, "onLost"); + mResultReceived = true; + onFailed(NETWORK_REQUEST_FAILED_LOST); + } + + @Override + @CallSuper + public void onAvailable(Network network) { + super.onAvailable(network); + mResultReceived = true; + } + + @CallSuper + public void onUnavailable() { + // TODO: b/32637799 this is hidden, do we really need this? + mResultReceived = true; + onFailed(NETWORK_REQUEST_FAILED_TIMEOUT); + } + + public void requestNetwork() { + if (mRequestSent == true) { + VvmLog.e(TAG, "requestNetwork() called twice"); + return; + } + mRequestSent = true; + getConnectivityManager().requestNetwork(getNetworkRequest(), this); + /** + * Somehow requestNetwork() with timeout doesn't work, and it's a hidden method. + * Implement our own timeout mechanism instead. + */ + Handler handler = new Handler(Looper.getMainLooper()); + handler.postDelayed(new Runnable() { + @Override + public void run() { + if (mResultReceived == false) { + onFailed(NETWORK_REQUEST_FAILED_TIMEOUT); + } + } + }, NETWORK_REQUEST_TIMEOUT_MILLIS); + } + + public void releaseNetwork() { + VvmLog.d(TAG, "releaseNetwork"); + getConnectivityManager().unregisterNetworkCallback(this); + } + + public ConnectivityManager getConnectivityManager() { + if (mConnectivityManager == null) { + mConnectivityManager = (ConnectivityManager) mContext.getSystemService( + Context.CONNECTIVITY_SERVICE); + } + return mConnectivityManager; + } + + @CallSuper + public void onFailed(String reason) { + VvmLog.d(TAG, "onFailed: " + reason); + if (mCarrierConfigHelper.isCellularDataRequired()) { + mCarrierConfigHelper + .handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION_CELLULAR_REQUIRED); + } else { + mCarrierConfigHelper.handleEvent(mStatus, OmtpEvents.DATA_NO_CONNECTION); + } + releaseNetwork(); + } +} diff --git a/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java b/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java new file mode 100644 index 000000000..eda7c4ee3 --- /dev/null +++ b/java/com/android/voicemailomtp/utils/IndentingPrintWriter.java @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.utils; + +import java.io.PrintWriter; +import java.io.Writer; +import java.util.Arrays; + +/** + * Lightweight wrapper around {@link PrintWriter} that automatically indents newlines based on + * internal state. It also automatically wraps long lines based on given line length. <p> Delays + * writing indent until first actual write on a newline, enabling indent modification after + * newline. + */ +public class IndentingPrintWriter extends PrintWriter { + + private final String mSingleIndent; + private final int mWrapLength; + + /** + * Mutable version of current indent + */ + private StringBuilder mIndentBuilder = new StringBuilder(); + /** + * Cache of current {@link #mIndentBuilder} value + */ + private char[] mCurrentIndent; + /** + * Length of current line being built, excluding any indent + */ + private int mCurrentLength; + + /** + * Flag indicating if we're currently sitting on an empty line, and that next write should be + * prefixed with the current indent. + */ + private boolean mEmptyLine = true; + + private char[] mSingleChar = new char[1]; + + public IndentingPrintWriter(Writer writer, String singleIndent) { + this(writer, singleIndent, -1); + } + + public IndentingPrintWriter(Writer writer, String singleIndent, int wrapLength) { + super(writer); + mSingleIndent = singleIndent; + mWrapLength = wrapLength; + } + + public void increaseIndent() { + mIndentBuilder.append(mSingleIndent); + mCurrentIndent = null; + } + + public void decreaseIndent() { + mIndentBuilder.delete(0, mSingleIndent.length()); + mCurrentIndent = null; + } + + public void printPair(String key, Object value) { + print(key + "=" + String.valueOf(value) + " "); + } + + public void printPair(String key, Object[] value) { + print(key + "=" + Arrays.toString(value) + " "); + } + + public void printHexPair(String key, int value) { + print(key + "=0x" + Integer.toHexString(value) + " "); + } + + @Override + public void println() { + write('\n'); + } + + @Override + public void write(int c) { + mSingleChar[0] = (char) c; + write(mSingleChar, 0, 1); + } + + @Override + public void write(String s, int off, int len) { + final char[] buf = new char[len]; + s.getChars(off, len - off, buf, 0); + write(buf, 0, len); + } + + @Override + public void write(char[] buf, int offset, int count) { + final int indentLength = mIndentBuilder.length(); + final int bufferEnd = offset + count; + int lineStart = offset; + int lineEnd = offset; + + // March through incoming buffer looking for newlines + while (lineEnd < bufferEnd) { + char ch = buf[lineEnd++]; + mCurrentLength++; + if (ch == '\n') { + maybeWriteIndent(); + super.write(buf, lineStart, lineEnd - lineStart); + lineStart = lineEnd; + mEmptyLine = true; + mCurrentLength = 0; + } + + // Wrap if we've pushed beyond line length + if (mWrapLength > 0 && mCurrentLength >= mWrapLength - indentLength) { + if (!mEmptyLine) { + // Give ourselves a fresh line to work with + super.write('\n'); + mEmptyLine = true; + mCurrentLength = lineEnd - lineStart; + } else { + // We need more than a dedicated line, slice it hard + maybeWriteIndent(); + super.write(buf, lineStart, lineEnd - lineStart); + super.write('\n'); + mEmptyLine = true; + lineStart = lineEnd; + mCurrentLength = 0; + } + } + } + + if (lineStart != lineEnd) { + maybeWriteIndent(); + super.write(buf, lineStart, lineEnd - lineStart); + } + } + + private void maybeWriteIndent() { + if (mEmptyLine) { + mEmptyLine = false; + if (mIndentBuilder.length() != 0) { + if (mCurrentIndent == null) { + mCurrentIndent = mIndentBuilder.toString().toCharArray(); + } + super.write(mCurrentIndent, 0, mCurrentIndent.length); + } + } + } +}
\ No newline at end of file diff --git a/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java b/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java new file mode 100644 index 000000000..f94070ecd --- /dev/null +++ b/java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.utils; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.net.Uri; +import android.provider.VoicemailContract.Voicemails; +import android.telecom.PhoneAccountHandle; +import com.android.voicemailomtp.Voicemail; +import java.util.List; + +public class VoicemailDatabaseUtil { + + /** + * Inserts a new voicemail into the voicemail content provider. + * + * @param context The context of the app doing the inserting + * @param voicemail Data to be inserted + * @return {@link Uri} of the newly inserted {@link Voicemail} + * @hide + */ + public static Uri insert(Context context, Voicemail voicemail) { + ContentResolver contentResolver = context.getContentResolver(); + ContentValues contentValues = getContentValues(voicemail); + return contentResolver + .insert(Voicemails.buildSourceUri(context.getPackageName()), contentValues); + } + + /** + * Inserts a list of voicemails into the voicemail content provider. + * + * @param context The context of the app doing the inserting + * @param voicemails Data to be inserted + * @return the number of voicemails inserted + * @hide + */ + public static int insert(Context context, List<Voicemail> voicemails) { + ContentResolver contentResolver = context.getContentResolver(); + int count = voicemails.size(); + for (int i = 0; i < count; i++) { + ContentValues contentValues = getContentValues(voicemails.get(i)); + contentResolver + .insert(Voicemails.buildSourceUri(context.getPackageName()), contentValues); + } + return count; + } + + + /** + * Maps structured {@link Voicemail} to {@link ContentValues} in content provider. + */ + private static ContentValues getContentValues(Voicemail voicemail) { + ContentValues contentValues = new ContentValues(); + contentValues.put(Voicemails.DATE, String.valueOf(voicemail.getTimestampMillis())); + contentValues.put(Voicemails.NUMBER, voicemail.getNumber()); + contentValues.put(Voicemails.DURATION, String.valueOf(voicemail.getDuration())); + contentValues.put(Voicemails.SOURCE_PACKAGE, voicemail.getSourcePackage()); + contentValues.put(Voicemails.SOURCE_DATA, voicemail.getSourceData()); + contentValues.put(Voicemails.IS_READ, voicemail.isRead() ? 1 : 0); + + PhoneAccountHandle phoneAccount = voicemail.getPhoneAccount(); + if (phoneAccount != null) { + contentValues.put(Voicemails.PHONE_ACCOUNT_COMPONENT_NAME, + phoneAccount.getComponentName().flattenToString()); + contentValues.put(Voicemails.PHONE_ACCOUNT_ID, phoneAccount.getId()); + } + + if (voicemail.getTranscription() != null) { + contentValues.put(Voicemails.TRANSCRIPTION, voicemail.getTranscription()); + } + + return contentValues; + } +} diff --git a/java/com/android/voicemailomtp/utils/VvmDumpHandler.java b/java/com/android/voicemailomtp/utils/VvmDumpHandler.java new file mode 100644 index 000000000..5768a9c19 --- /dev/null +++ b/java/com/android/voicemailomtp/utils/VvmDumpHandler.java @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.utils; + +import android.content.Context; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; + +import com.android.voicemailomtp.OmtpVvmCarrierConfigHelper; +import com.android.voicemailomtp.VvmLog; + +import java.io.FileDescriptor; +import java.io.PrintWriter; + +public class VvmDumpHandler { + + public static void dump(Context context, FileDescriptor fd, PrintWriter writer, + String[] args) { + IndentingPrintWriter indentedWriter = new IndentingPrintWriter(writer, " "); + indentedWriter.println("******* OmtpVvm *******"); + indentedWriter.println("======= Configs ======="); + indentedWriter.increaseIndent(); + for (PhoneAccountHandle handle : context.getSystemService(TelecomManager.class) + .getCallCapablePhoneAccounts()) { + OmtpVvmCarrierConfigHelper config = new OmtpVvmCarrierConfigHelper(context, handle); + indentedWriter.println(config.toString()); + } + indentedWriter.decreaseIndent(); + indentedWriter.println("======== Logs ========="); + VvmLog.dump(fd, indentedWriter, args); + } +} diff --git a/java/com/android/voicemailomtp/utils/XmlUtils.java b/java/com/android/voicemailomtp/utils/XmlUtils.java new file mode 100644 index 000000000..768247e27 --- /dev/null +++ b/java/com/android/voicemailomtp/utils/XmlUtils.java @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2016 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.android.voicemailomtp.utils; + +import android.util.ArrayMap; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +public class XmlUtils { + + public static final ArrayMap<String, ?> readThisArrayMapXml(XmlPullParser parser, String endTag, + String[] name, ReadMapCallback callback) + throws XmlPullParserException, java.io.IOException { + ArrayMap<String, Object> map = new ArrayMap<>(); + + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + Object val = readThisValueXml(parser, name, callback, true); + map.put(name[0], val); + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(endTag)) { + return map; + } + throw new XmlPullParserException( + "Expected " + endTag + " end tag at: " + parser.getName()); + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + + throw new XmlPullParserException( + "Document ended before " + endTag + " end tag"); + } + + /** + * Read an ArrayList object from an XmlPullParser. The XML data could previously have been + * generated by writeListXml(). The XmlPullParser must be positioned <em>after</em> the tag + * that begins the list. + * + * @param parser The XmlPullParser from which to read the list data. + * @param endTag Name of the tag that will end the list, usually "list". + * @param name An array of one string, used to return the name attribute of the list's tag. + * @return HashMap The newly generated list. + */ + public static final ArrayList readThisListXml(XmlPullParser parser, String endTag, + String[] name, ReadMapCallback callback, boolean arrayMap) + throws XmlPullParserException, java.io.IOException { + ArrayList list = new ArrayList(); + + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + Object val = readThisValueXml(parser, name, callback, arrayMap); + list.add(val); + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(endTag)) { + return list; + } + throw new XmlPullParserException( + "Expected " + endTag + " end tag at: " + parser.getName()); + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + + throw new XmlPullParserException( + "Document ended before " + endTag + " end tag"); + } + + /** + * Read a String[] object from an XmlPullParser. The XML data could previously have been + * generated by writeStringArrayXml(). The XmlPullParser must be positioned <em>after</em> the + * tag that begins the list. + * + * @param parser The XmlPullParser from which to read the list data. + * @param endTag Name of the tag that will end the list, usually "string-array". + * @param name An array of one string, used to return the name attribute of the list's tag. + * @return Returns a newly generated String[]. + */ + public static String[] readThisStringArrayXml(XmlPullParser parser, String endTag, + String[] name) throws XmlPullParserException, java.io.IOException { + + parser.next(); + + List<String> array = new ArrayList<>(); + + int eventType = parser.getEventType(); + do { + if (eventType == XmlPullParser.START_TAG) { + if (parser.getName().equals("item")) { + try { + array.add(parser.getAttributeValue(null, "value")); + } catch (NullPointerException e) { + throw new XmlPullParserException("Need value attribute in item"); + } catch (NumberFormatException e) { + throw new XmlPullParserException("Not a number in value attribute in item"); + } + } else { + throw new XmlPullParserException("Expected item tag at: " + parser.getName()); + } + } else if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(endTag)) { + return array.toArray(new String[0]); + } else if (parser.getName().equals("item")) { + + } else { + throw new XmlPullParserException("Expected " + endTag + " end tag at: " + + parser.getName()); + } + } + eventType = parser.next(); + } while (eventType != XmlPullParser.END_DOCUMENT); + + throw new XmlPullParserException("Document ended before " + endTag + " end tag"); + } + + private static Object readThisValueXml(XmlPullParser parser, String[] name, + ReadMapCallback callback, boolean arrayMap) + throws XmlPullParserException, java.io.IOException { + final String valueName = parser.getAttributeValue(null, "name"); + final String tagName = parser.getName(); + + Object res; + + if (tagName.equals("null")) { + res = null; + } else if (tagName.equals("string")) { + String value = ""; + int eventType; + while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals("string")) { + name[0] = valueName; + return value; + } + throw new XmlPullParserException( + "Unexpected end tag in <string>: " + parser.getName()); + } else if (eventType == XmlPullParser.TEXT) { + value += parser.getText(); + } else if (eventType == XmlPullParser.START_TAG) { + throw new XmlPullParserException( + "Unexpected start tag in <string>: " + parser.getName()); + } + } + throw new XmlPullParserException( + "Unexpected end of document in <string>"); + } else if ((res = readThisPrimitiveValueXml(parser, tagName)) != null) { + // all work already done by readThisPrimitiveValueXml + } else if (tagName.equals("string-array")) { + res = readThisStringArrayXml(parser, "string-array", name); + name[0] = valueName; + return res; + } else if (tagName.equals("list")) { + parser.next(); + res = readThisListXml(parser, "list", name, callback, arrayMap); + name[0] = valueName; + return res; + } else if (callback != null) { + res = callback.readThisUnknownObjectXml(parser, tagName); + name[0] = valueName; + return res; + } else { + throw new XmlPullParserException("Unknown tag: " + tagName); + } + + // Skip through to end tag. + int eventType; + while ((eventType = parser.next()) != XmlPullParser.END_DOCUMENT) { + if (eventType == XmlPullParser.END_TAG) { + if (parser.getName().equals(tagName)) { + name[0] = valueName; + return res; + } + throw new XmlPullParserException( + "Unexpected end tag in <" + tagName + ">: " + parser.getName()); + } else if (eventType == XmlPullParser.TEXT) { + throw new XmlPullParserException( + "Unexpected text in <" + tagName + ">: " + parser.getName()); + } else if (eventType == XmlPullParser.START_TAG) { + throw new XmlPullParserException( + "Unexpected start tag in <" + tagName + ">: " + parser.getName()); + } + } + throw new XmlPullParserException( + "Unexpected end of document in <" + tagName + ">"); + } + + private static final Object readThisPrimitiveValueXml(XmlPullParser parser, String tagName) + throws XmlPullParserException, java.io.IOException { + try { + if (tagName.equals("int")) { + return Integer.parseInt(parser.getAttributeValue(null, "value")); + } else if (tagName.equals("long")) { + return Long.valueOf(parser.getAttributeValue(null, "value")); + } else if (tagName.equals("float")) { + return Float.valueOf(parser.getAttributeValue(null, "value")); + } else if (tagName.equals("double")) { + return Double.valueOf(parser.getAttributeValue(null, "value")); + } else if (tagName.equals("boolean")) { + return Boolean.valueOf(parser.getAttributeValue(null, "value")); + } else { + return null; + } + } catch (NullPointerException e) { + throw new XmlPullParserException("Need value attribute in <" + tagName + ">"); + } catch (NumberFormatException e) { + throw new XmlPullParserException( + "Not a number in value attribute in <" + tagName + ">"); + } + } + + public interface ReadMapCallback { + + /** + * Called from readThisMapXml when a START_TAG is not recognized. The input stream is + * positioned within the start tag so that attributes can be read using in.getAttribute. + * + * @param in the XML input stream + * @param tag the START_TAG that was not recognized. + * @return the Object parsed from the stream which will be put into the map. + * @throws XmlPullParserException if the START_TAG is not recognized. + * @throws IOException on XmlPullParser serialization errors. + */ + Object readThisUnknownObjectXml(XmlPullParser in, String tag) + throws XmlPullParserException, IOException; + } +}
\ No newline at end of file |