summaryrefslogtreecommitdiff
path: root/java/com/android/voicemailomtp
diff options
context:
space:
mode:
authorEric Erfanian <erfanian@google.com>2017-02-22 16:32:36 -0800
committerEric Erfanian <erfanian@google.com>2017-03-01 09:56:52 -0800
commitccca31529c07970e89419fb85a9e8153a5396838 (patch)
treea7034c0a01672b97728c13282a2672771cd28baa /java/com/android/voicemailomtp
parente7ae4624ba6f25cb8e648db74e0d64c0113a16ba (diff)
Update dialer sources.
Test: Built package and system image. This change clobbers the old source, and is an export from an internal Google repository. The internal repository was forked form Android in March, and this change includes modifications since then, to near the v8 release. Since the fork, we've moved code from monolithic to independent modules. In addition, we've switched to Blaze/Bazel as the build sysetm. This export, however, still uses make. New dependencies have been added: - Dagger - Auto-Value - Glide - Libshortcutbadger Going forward, development will still be in Google3, and the Gerrit release will become an automated export, with the next drop happening in ~ two weeks. Android.mk includes local modifications from ToT. Abridged changelog: Bug fixes ● Not able to mute, add a call when using Phone app in multiwindow mode ● Double tap on keypad triggering multiple key and tones ● Reported spam numbers not showing as spam in the call log ● Crash when user tries to block number while Phone app is not set as default ● Crash when user picks a number from search auto-complete list Visual Voicemail (VVM) improvements ● Share Voicemail audio via standard exporting mechanisms that support file attachment (email, MMS, etc.) ● Make phone number, email and web sites in VVM transcript clickable ● Set PIN before declining VVM Terms of Service {Carrier} ● Set client type for outbound visual voicemail SMS {Carrier} New incoming call and incall UI on older devices (Android M) ● Updated Phone app icon ● New incall UI (large buttons, button labels) ● New and animated Answer/Reject gestures Accessibility ● Add custom answer/decline call buttons on answer screen for touch exploration accessibility services ● Increase size of touch target ● Add verbal feedback when a Voicemail fails to load ● Fix pressing of Phone buttons while in a phone call using Switch Access ● Fix selecting and opening contacts in talkback mode ● Split focus for ‘Learn More’ link in caller id & spam to help distinguish similar text Other ● Backup & Restore for App Preferences ● Prompt user to enable Wi-Fi calling if the call ends due to out of service and Wi-Fi is connected ● Rename “Dialpad” to “Keypad” ● Show "Private number" for restricted calls ● Delete unused items (vcard, add contact, call history) from Phone menu Change-Id: I2a7e53532a24c21bf308bf0a6d178d7ddbca4958
Diffstat (limited to 'java/com/android/voicemailomtp')
-rw-r--r--java/com/android/voicemailomtp/ActivationTask.java305
-rw-r--r--java/com/android/voicemailomtp/AndroidManifest.xml105
-rw-r--r--java/com/android/voicemailomtp/Assert.java62
-rw-r--r--java/com/android/voicemailomtp/DefaultOmtpEventHandler.java202
-rw-r--r--java/com/android/voicemailomtp/NeededForTesting.java25
-rw-r--r--java/com/android/voicemailomtp/OmtpConstants.java248
-rw-r--r--java/com/android/voicemailomtp/OmtpEvents.java156
-rw-r--r--java/com/android/voicemailomtp/OmtpService.java65
-rw-r--r--java/com/android/voicemailomtp/OmtpVvmCarrierConfigHelper.java423
-rw-r--r--java/com/android/voicemailomtp/SubscriptionInfoHelper.java75
-rw-r--r--java/com/android/voicemailomtp/TelephonyManagerStub.java80
-rw-r--r--java/com/android/voicemailomtp/TelephonyVvmConfigManager.java154
-rw-r--r--java/com/android/voicemailomtp/VisualVoicemailPreferences.java143
-rw-r--r--java/com/android/voicemailomtp/Voicemail.java330
-rw-r--r--java/com/android/voicemailomtp/VoicemailStatus.java158
-rw-r--r--java/com/android/voicemailomtp/VvmLog.java179
-rw-r--r--java/com/android/voicemailomtp/VvmPackageInstallReceiver.java70
-rw-r--r--java/com/android/voicemailomtp/VvmPhoneStateListener.java103
-rw-r--r--java/com/android/voicemailomtp/fetch/FetchVoicemailReceiver.java219
-rw-r--r--java/com/android/voicemailomtp/fetch/VoicemailFetchedCallback.java101
-rw-r--r--java/com/android/voicemailomtp/imap/ImapHelper.java711
-rw-r--r--java/com/android/voicemailomtp/imap/VoicemailPayload.java38
-rw-r--r--java/com/android/voicemailomtp/mail/Address.java541
-rw-r--r--java/com/android/voicemailomtp/mail/AuthenticationFailedException.java33
-rw-r--r--java/com/android/voicemailomtp/mail/Base64Body.java62
-rw-r--r--java/com/android/voicemailomtp/mail/Body.java25
-rw-r--r--java/com/android/voicemailomtp/mail/BodyPart.java24
-rw-r--r--java/com/android/voicemailomtp/mail/CertificateValidationException.java29
-rw-r--r--java/com/android/voicemailomtp/mail/FetchProfile.java84
-rw-r--r--java/com/android/voicemailomtp/mail/Fetchable.java23
-rw-r--r--java/com/android/voicemailomtp/mail/FixedLengthInputStream.java79
-rw-r--r--java/com/android/voicemailomtp/mail/Flag.java29
-rw-r--r--java/com/android/voicemailomtp/mail/MailTransport.java344
-rw-r--r--java/com/android/voicemailomtp/mail/MeetingInfo.java29
-rw-r--r--java/com/android/voicemailomtp/mail/Message.java144
-rw-r--r--java/com/android/voicemailomtp/mail/MessageDateComparator.java34
-rw-r--r--java/com/android/voicemailomtp/mail/MessagingException.java139
-rw-r--r--java/com/android/voicemailomtp/mail/Multipart.java62
-rw-r--r--java/com/android/voicemailomtp/mail/PackedString.java175
-rw-r--r--java/com/android/voicemailomtp/mail/Part.java51
-rw-r--r--java/com/android/voicemailomtp/mail/PeekableInputStream.java80
-rw-r--r--java/com/android/voicemailomtp/mail/TempDirectory.java41
-rw-r--r--java/com/android/voicemailomtp/mail/internet/BinaryTempFileBody.java91
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeBodyPart.java207
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeHeader.java161
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeMessage.java675
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeMultipart.java112
-rw-r--r--java/com/android/voicemailomtp/mail/internet/MimeUtility.java416
-rw-r--r--java/com/android/voicemailomtp/mail/internet/TextBody.java63
-rw-r--r--java/com/android/voicemailomtp/mail/store/ImapConnection.java413
-rw-r--r--java/com/android/voicemailomtp/mail/store/ImapFolder.java784
-rw-r--r--java/com/android/voicemailomtp/mail/store/ImapStore.java176
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/DigestMd5Utils.java335
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapConstants.java144
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapElement.java120
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapList.java235
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapMemoryLiteral.java76
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapResponse.java158
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapResponseParser.java432
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapSimpleString.java62
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapString.java192
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapTempFileLiteral.java123
-rw-r--r--java/com/android/voicemailomtp/mail/store/imap/ImapUtility.java125
-rw-r--r--java/com/android/voicemailomtp/mail/utility/CountingOutputStream.java48
-rw-r--r--java/com/android/voicemailomtp/mail/utility/EOLConvertingOutputStream.java48
-rw-r--r--java/com/android/voicemailomtp/mail/utils/LogUtils.java413
-rw-r--r--java/com/android/voicemailomtp/mail/utils/Utility.java80
-rw-r--r--java/com/android/voicemailomtp/permissions.xml21
-rw-r--r--java/com/android/voicemailomtp/protocol/CvvmProtocol.java59
-rw-r--r--java/com/android/voicemailomtp/protocol/OmtpProtocol.java37
-rw-r--r--java/com/android/voicemailomtp/protocol/ProtocolHelper.java43
-rw-r--r--java/com/android/voicemailomtp/protocol/VisualVoicemailProtocol.java100
-rw-r--r--java/com/android/voicemailomtp/protocol/VisualVoicemailProtocolFactory.java47
-rw-r--r--java/com/android/voicemailomtp/protocol/Vvm3EventHandler.java271
-rw-r--r--java/com/android/voicemailomtp/protocol/Vvm3Protocol.java301
-rw-r--r--java/com/android/voicemailomtp/protocol/Vvm3Subscriber.java326
-rw-r--r--java/com/android/voicemailomtp/res/layout/voicemail_change_pin.xml97
-rw-r--r--java/com/android/voicemailomtp/res/values/arrays.xml19
-rw-r--r--java/com/android/voicemailomtp/res/values/attrs.xml20
-rw-r--r--java/com/android/voicemailomtp/res/values/colors.xml19
-rw-r--r--java/com/android/voicemailomtp/res/values/config.xml19
-rw-r--r--java/com/android/voicemailomtp/res/values/dimens.xml19
-rw-r--r--java/com/android/voicemailomtp/res/values/ids.xml20
-rw-r--r--java/com/android/voicemailomtp/res/values/strings.xml86
-rw-r--r--java/com/android/voicemailomtp/res/values/styles.xml19
-rw-r--r--java/com/android/voicemailomtp/res/xml/voicemail_settings.xml27
-rw-r--r--java/com/android/voicemailomtp/res/xml/vvm_config.xml134
-rw-r--r--java/com/android/voicemailomtp/scheduling/BaseTask.java206
-rw-r--r--java/com/android/voicemailomtp/scheduling/BlockerTask.java55
-rw-r--r--java/com/android/voicemailomtp/scheduling/MinimalIntervalPolicy.java69
-rw-r--r--java/com/android/voicemailomtp/scheduling/Policy.java36
-rw-r--r--java/com/android/voicemailomtp/scheduling/PostponePolicy.java69
-rw-r--r--java/com/android/voicemailomtp/scheduling/RetryPolicy.java117
-rw-r--r--java/com/android/voicemailomtp/scheduling/Task.java133
-rw-r--r--java/com/android/voicemailomtp/scheduling/TaskSchedulerService.java392
-rw-r--r--java/com/android/voicemailomtp/settings/VisualVoicemailSettingsUtil.java77
-rw-r--r--java/com/android/voicemailomtp/settings/VoicemailChangePinActivity.java634
-rw-r--r--java/com/android/voicemailomtp/settings/VoicemailSettingsActivity.java222
-rw-r--r--java/com/android/voicemailomtp/sms/LegacyModeSmsHandler.java67
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpCvvmMessageSender.java55
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpMessageReceiver.java162
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpMessageSender.java89
-rw-r--r--java/com/android/voicemailomtp/sms/OmtpStandardMessageSender.java119
-rw-r--r--java/com/android/voicemailomtp/sms/StatusMessage.java209
-rw-r--r--java/com/android/voicemailomtp/sms/StatusSmsFetcher.java162
-rw-r--r--java/com/android/voicemailomtp/sms/SyncMessage.java166
-rw-r--r--java/com/android/voicemailomtp/sms/Vvm3MessageSender.java56
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/commons/io/IOUtils.java1202
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/BodyDescriptor.java392
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/CloseShieldInputStream.java129
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/ContentHandler.java177
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/EOLConvertingInputStream.java139
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/Log.java114
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/LogFactory.java29
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeBoundaryInputStream.java184
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/MimeStreamParser.java324
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/RootInputStream.java111
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/codec/EncoderUtil.java630
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/Base64InputStream.java151
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/ByteQueue.java62
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/DecoderUtil.java284
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/QuotedPrintableInputStream.java229
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/decoder/UnboundedFifoByteBuffer.java272
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/AddressListField.java65
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTransferEncodingField.java88
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/ContentTypeField.java259
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DateTimeField.java73
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DefaultFieldParser.java45
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/DelegatingFieldParser.java47
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/Field.java192
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/FieldParser.java21
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxField.java70
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/MailboxListField.java67
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/UnstructuredField.java49
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Address.java52
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/AddressList.java138
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Builder.java243
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/DomainList.java76
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Group.java75
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/Mailbox.java121
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/MailboxList.java71
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/NamedMailbox.java71
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddr_spec.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTaddress_list.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTangle_addr.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTdomain.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTgroup_body.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTlocal_part.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTmailbox.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTname_addr.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTphrase.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ASTroute.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.java977
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParser.jj595
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserConstants.java76
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTokenManager.java1009
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserTreeConstants.java35
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/AddressListParserVisitor.java19
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/BaseNode.java30
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/JJTAddressListParserState.java123
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Node.java37
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/ParseException.java207
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleCharStream.java454
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/SimpleNode.java87
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/Token.java96
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/address/parser/TokenMgrError.java148
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParser.java268
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserConstants.java62
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ContentTypeParserTokenManager.java877
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/ParseException.java207
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/SimpleCharStream.java454
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/Token.java96
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/contenttype/parser/TokenMgrError.java148
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/DateTime.java127
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParser.java570
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserConstants.java86
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/DateTimeParserTokenManager.java882
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/ParseException.java207
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/SimpleCharStream.java454
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/Token.java96
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/field/datetime/parser/TokenMgrError.java148
-rw-r--r--java/com/android/voicemailomtp/src/org/apache/james/mime4j/util/CharsetUtil.java1249
-rw-r--r--java/com/android/voicemailomtp/sync/OmtpVvmSourceManager.java120
-rw-r--r--java/com/android/voicemailomtp/sync/OmtpVvmSyncReceiver.java61
-rw-r--r--java/com/android/voicemailomtp/sync/OmtpVvmSyncService.java278
-rw-r--r--java/com/android/voicemailomtp/sync/SyncOneTask.java82
-rw-r--r--java/com/android/voicemailomtp/sync/SyncTask.java79
-rw-r--r--java/com/android/voicemailomtp/sync/UploadTask.java68
-rw-r--r--java/com/android/voicemailomtp/sync/VoicemailProviderChangeReceiver.java41
-rw-r--r--java/com/android/voicemailomtp/sync/VoicemailStatusQueryHelper.java113
-rw-r--r--java/com/android/voicemailomtp/sync/VoicemailsQueryHelper.java244
-rw-r--r--java/com/android/voicemailomtp/sync/VvmNetworkRequest.java118
-rw-r--r--java/com/android/voicemailomtp/sync/VvmNetworkRequestCallback.java171
-rw-r--r--java/com/android/voicemailomtp/utils/IndentingPrintWriter.java160
-rw-r--r--java/com/android/voicemailomtp/utils/VoicemailDatabaseUtil.java90
-rw-r--r--java/com/android/voicemailomtp/utils/VvmDumpHandler.java46
-rw-r--r--java/com/android/voicemailomtp/utils/XmlUtils.java245
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 &lt; 0 or &gt;= 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