diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index def30cafb8..1ba618734f 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -23,3 +23,4 @@ FairEmail uses: * [Material design icons](https://github.com/google/material-design-icons). Copyright ???. [Apache license version 2.0](https://github.com/google/material-design-icons#user-content-license). * [CSS Parser](http://cssparser.sourceforge.net/). Copyright © 1999–2019. All rights reserved. [Apache License, Version 2.0](http://cssparser.sourceforge.net/licenses.html). * [Java™ Architecture for XML Binding](https://github.com/eclipse-ee4j/jaxb-ri). Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved. [GNU General Public License Version 2](https://github.com/eclipse-ee4j/jaxb-ri/blob/master/jaxb-ri/LICENSE.md). +* [File Icon Images](https://github.com/dmhendricks/file-icon-vectors). A collection of file type/extension SVG icons, available free for use in your applications. [MIT License](https://github.com/dmhendricks/file-icon-vectors/blob/master/LICENSE). diff --git a/FAQ.md b/FAQ.md index 06e212b2aa..033bb53a85 100644 --- a/FAQ.md +++ b/FAQ.md @@ -29,7 +29,8 @@ for instance when two factor authentication is enabled. For authorizing: * Gmail / G suite: see [question 6](#user-content-faq6) -* Outlook / Hotmail: see [question 14](#user-content-faq14) +* Outlook / Live / Hotmail: see [question 14](#user-content-faq14) +* Office365: see [question 14](#user-content-faq156) * Microsoft Exchange: see [question 8](#user-content-faq8) * Yahoo!: see [question 88](#user-content-faq88) * Apple iCloud: see [question 148](#user-content-faq148) @@ -108,7 +109,7 @@ Related questions: * ~~Unified starred messages view~~ (there is already a special search for this) * ~~Notification move action~~ * ~~S/MIME support~~ -* Search for settings: low priority +* ~~Search for settings~~ Anything on this list is in random order and *might* be added in the near future. @@ -134,7 +135,7 @@ Fonts, sizes, colors, etc should be material design whenever possible. * [~~(10) What does 'UIDPLUS not supported' mean?~~](#user-content-faq10) * [(12) How does encryption/decryption work?](#user-content-faq12) * [(13) How does search on device/server work?](#user-content-faq13) -* [(14) How can I setup Outlook / Live / Hotmail with 2FA?](#user-content-faq14) +* [(14) How can I set up an Outlook / Live / Hotmail account?](#user-content-faq14) * [(15) Why does the message text keep loading?](#user-content-faq15) * [(16) Why are messages not being synchronized?](#user-content-faq16) * [~~(17) Why does manual synchronize not work?~~](#user-content-faq17) @@ -237,7 +238,7 @@ Fonts, sizes, colors, etc should be material design whenever possible. * [~~(116) How can I show images in messages from trusted senders by default?~~](#user-content-faq116) * [(117) Can you help me restore my purchase?](#user-content-faq117) * [(118) What does 'Remove tracking parameters' exactly?](#user-content-faq118) -* [(119) Can you add colors to the unified inbox widget?](#user-content-faq119) +* [~~(119) Can you add colors to the unified inbox widget?~~](#user-content-faq119) * [(120) Why are new message notifications not removed on opening the app?](#user-content-faq120) * [(121) How are messages grouped into a conversation?](#user-content-faq121) * [~~(122) Why is the recipient name/email address show with a warning color?~~](#user-content-faq122) @@ -274,6 +275,7 @@ Fonts, sizes, colors, etc should be material design whenever possible. * [(153) Why does permanently deleting Gmail message not work?](#user-content-faq153) * [(154) Can you add favicons as contact photos?](#user-content-faq154) * [(155) What is a winmail.dat file?](#user-content-faq155) +* [(156) How can I set up an Office365 account?](#user-content-faq156) [I have another question.](#user-content-support) @@ -783,13 +785,17 @@ Searching messages on the device is a free feature, searching messages on the se
-**(14) How can I setup Outlook / Live / Hotmail with 2FA?** +**(14) How can I set up an Outlook / Live / Hotmail account?** + +An Outlook / Live / Hotmail account can be set up via the quick setup wizard and selecting *Outlook*. To use an Outlook, Live or Hotmail account with two factor authentication enabled, you need to create an app password. See [here](https://support.microsoft.com/en-us/help/12409/microsoft-account-app-passwords-two-step-verification) for the details. See [here](https://support.office.com/en-us/article/pop-imap-and-smtp-settings-for-outlook-com-d088b986-291d-42b8-9564-9c414e2aa040) for Microsoft's instructions. +For setting up an Office365 account, please see [this FAQ](#user-content-faq156). +
@@ -1160,14 +1166,14 @@ You can use the [Email Privacy Tester](https://www.emailprivacytester.com/) for Most providers accept validated addresses only when sending messages to prevent spam. -For example Google modifies the message headers like this: +For example Google modifies the message headers like this for *unverified* addresses: ``` From: Somebody X-Google-Original-From: Somebody ``` -This means that the edited sender address was automatically replaced by a validated address before sending the message. +This means that the edited sender address was automatically replaced by a verified address before sending the message. Note that this is independent of receiving messages. @@ -2224,7 +2230,7 @@ However, picking contacts is delegated to and done by Android and not by FairEma **(99) Can you add a rich text or markdown editor?** -FairEmail provides common text formatting (bold, italic, underline, text size and color) via the Android text selection menu. +FairEmail provides common text formatting (bold, italic, underline, text size and color) via a toolbar that appears after selecting some text. A [Rich text](https://en.wikipedia.org/wiki/Formatted_text) or [Markdown](https://en.wikipedia.org/wiki/Markdown) editor would not be used by many people on a small mobile device and, more important, @@ -2527,12 +2533,12 @@ Checking *Remove tracking parameters* will remove all [UTM parameters](https://e
-**(119) Can you add colors to the unified inbox widget?** +**~~(119) Can you add colors to the unified inbox widget?~~** -The widget is designed to look good on most home/launcher screens by making it monochrome and by using a half transparent background. -This way the widget will nicely blend in, while still being properly readable. +~~The widget is designed to look good on most home/launcher screens by making it monochrome and by using a half transparent background.~~ +~~This way the widget will nicely blend in, while still being properly readable.~~ -Adding colors will cause problems with some backgrounds and will cause readability problems, which is why this won't be added. +~~Adding colors will cause problems with some backgrounds and will cause readability problems, which is why this won't be added.~~ Due to Android limitations it is not possible to dynamically set the opacity of the background and to have rounded corners at the same time. @@ -3041,6 +3047,17 @@ You can view it with for example the Android app [Letter Opener](https://play.go
+ +**(156) How can I set up an Office365 account?** + +An Office365 account can be set up via the quick setup wizard and selecting *Office365 (OAuth)*. + +If the wizard ends with *AUTHENTICATE failed*, IMAP and/or SMTP might be disabled for the account. +In this case you should ask the administrator to enable IMAP and SMTP. +The procedure is documented [here](https://docs.microsoft.com/en-in/exchange/troubleshoot/configure-mailboxes/pop3-imap-owa-activesync-office-365). + +
+ ## Support Only the latest Play store version and latest GitHub release are supported. diff --git a/PLAYSTORE.txt b/PLAYSTORE.txt index b9c20c15b5..550343b3b8 100644 --- a/PLAYSTORE.txt +++ b/PLAYSTORE.txt @@ -17,7 +17,7 @@ FairEmail might be for you if you value your privacy. * Offline storage and operations * Battery friendly * Low data usage -* Small (~ 12 MB) +* Small (< 15 MB) * Material design (including dark/black theme) * Maintained and supported diff --git a/README.md b/README.md index ec4ddf4c6d..34d131e4d8 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ FairEmail might be for you if you value your privacy. * Offline storage and operations * Battery friendly * Low data usage -* Small (~ 12 MB) +* Small (< 15 MB) * Material design (including dark/black theme) * Maintained and supported diff --git a/app/build.gradle b/app/build.gradle index e18525bd35..1b7af61c0b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,18 +7,18 @@ def keystoreProperties = new Properties() keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) android { - compileSdkVersion 29 + compileSdkVersion 30 defaultConfig { applicationId "eu.faircode.email" minSdkVersion 21 - targetSdkVersion 29 - versionCode 1195 - versionName "1.1195" + targetSdkVersion 30 + versionCode 1205 + versionName "1.1205" archivesBaseName = "FairEmail-v$versionName" // https://en.wikipedia.org/wiki/List_of_dinosaur_genera - buildConfigField "String", "RELEASE_NAME", "\"Invictarx\"" + buildConfigField "String", "RELEASE_NAME", "\"Jeyawati\"" javaCompileOptions { annotationProcessorOptions { @@ -48,6 +48,18 @@ android { shaders = false } } + + sourceSets { + github { + java.srcDirs = ['src/main/java', 'src/iab/java'] + } + fdroid { + java.srcDirs = ['src/main/java', 'src/fdroid/java'] + } + play { + java.srcDirs = ['src/main/java', 'src/iab/java'] + } + } } dependenciesInfo { @@ -76,13 +88,13 @@ android { } signingConfigs { - play { + release { storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] keyAlias keystoreProperties['keyAlias'] keyPassword keystoreProperties['keyPassword'] } - github { + v1 { storeFile file(keystoreProperties['storeFile']) storePassword keystoreProperties['storePassword'] keyAlias keystoreProperties['keyAlias'] @@ -94,19 +106,12 @@ android { } buildTypes { - play { - debuggable = false - minifyEnabled = true - useProguard = true - proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.play - } - github { + release { debuggable = false minifyEnabled = true useProguard = true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' - signingConfig signingConfigs.github + signingConfig signingConfigs.release } debug { applicationIdSuffix '.debug' @@ -120,7 +125,7 @@ android { flavorDimensions "all" productFlavors { - full { + github { dimension "all" buildConfigField "boolean", "BETA_RELEASE", "true" buildConfigField "boolean", "PLAY_STORE_RELEASE", "false" @@ -133,24 +138,23 @@ android { buildConfigField "String", "RECORDER_URI", "\"https://f-droid.org/packages/com.github.axet.audiorecorder/\"" buildConfigField "String", "APPS_URI", "\"https://github.com/M66B?tab=repositories/\"" } - play_beta { + fdroid { dimension "all" - //minSdkVersion 23 buildConfigField "boolean", "BETA_RELEASE", "true" - buildConfigField "boolean", "PLAY_STORE_RELEASE", "true" - buildConfigField "String", "INVITE_URI", "\"https://play.google.com/store/apps/details?id=eu.faircode.email\"" - buildConfigField "String", "PRO_FEATURES_URI", "\"https://email.faircode.eu/#pro\"" - buildConfigField "String", "CHANGELOG", "\"\"" - buildConfigField "String", "GITHUB_LATEST_API", "\"\"" - buildConfigField "String", "OPENKEYCHAIN_URI", "\"https://play.google.com/store/apps/details?id=org.sufficientlysecure.keychain\"" - buildConfigField "String", "CAMERA_URI", "\"https://play.google.com/store/apps/details?id=net.sourceforge.opencamera\"" - buildConfigField "String", "RECORDER_URI", "\"https://play.google.com/store/apps/details?id=com.github.axet.audiorecorder\"" - buildConfigField "String", "APPS_URI", "\"https://play.google.com/store/apps/dev?id=8420080860664580239\"" + buildConfigField "boolean", "PLAY_STORE_RELEASE", "false" + buildConfigField "String", "INVITE_URI", "\"https://email.faircode.eu/\"" + buildConfigField "String", "PRO_FEATURES_URI", "\"https://email.faircode.eu/donate/\"" + buildConfigField "String", "CHANGELOG", "\"https://github.com/M66B/FairEmail/releases/\"" + buildConfigField "String", "GITHUB_LATEST_API", "\"https://api.github.com/repos/M66B/open-source-email/releases/latest\"" + buildConfigField "String", "OPENKEYCHAIN_URI", "\"https://f-droid.org/en/packages/org.sufficientlysecure.keychain/\"" + buildConfigField "String", "CAMERA_URI", "\"https://f-droid.org/en/packages/net.sourceforge.opencamera/\"" + buildConfigField "String", "RECORDER_URI", "\"https://f-droid.org/packages/com.github.axet.audiorecorder/\"" + buildConfigField "String", "APPS_URI", "\"https://github.com/M66B?tab=repositories/\"" } - play_release { + play { dimension "all" //minSdkVersion 23 - buildConfigField "boolean", "BETA_RELEASE", "false" + buildConfigField "boolean", "BETA_RELEASE", "true" buildConfigField "boolean", "PLAY_STORE_RELEASE", "true" buildConfigField "String", "INVITE_URI", "\"https://play.google.com/store/apps/details?id=eu.faircode.email\"" buildConfigField "String", "PRO_FEATURES_URI", "\"https://email.faircode.eu/#pro\"" @@ -165,15 +169,10 @@ android { variantFilter { variant -> def flavors = variant.flavors*.name - // Builds: play, github, debug - // Flavors: full, play_beta, play_release - if (variant.buildType.name == "play" && flavors.contains("full")) { - setIgnore(true) - } - if (variant.buildType.name == "github" && flavors.contains("play_beta")) { - setIgnore(true) - } - if (flavors.contains("play_release")) { + // Builds: release, debug + // Flavors: github, fdroid, play + if (variant.buildType.name == "debug" && + (flavors.contains("fdroid") || flavors.contains("play"))) { setIgnore(true) } } @@ -243,27 +242,27 @@ configurations.all { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - def core_version = "1.4.0-alpha01" + def core_version = "1.5.0-alpha01" def appcompat_version = "1.3.0-alpha01" - def fragment_version = "1.3.0-alpha05" + def fragment_version = "1.3.0-alpha06" def recyclerview_version = "1.2.0-alpha03" def coordinatorlayout_version = "1.1.0" def constraintlayout_version = "2.0.0-beta3" def material_version = "1.3.0-alpha01" - def browser_version = "1.3.0-alpha01" + def browser_version = "1.3.0-alpha03" def lbm_version = "1.0.0" def swiperefresh_version = "1.1.0-rc01" def documentfile_version = "1.0.1" - def lifecycle_version = "2.2.0" // 2.3.0-alpha03 + def lifecycle_version = "2.2.0" // 2.3.0-alpha04 def sqlite_version = "2.1.0" - def room_version = "2.2.5" - def paging_version = "2.1.2" + def room_version = "2.2.5" // 2.3.0-alpha01 + def paging_version = "2.1.2" // 3.0.0-alpha01 def preference_version = "1.1.1" def work_version = "2.4.0-beta01" def exif_version = "1.3.0-alpha01" def biometric_version = "1.0.1" def textclassifier_version = "1.0.0-alpha03" - def billingclient_version = "2.2.1" + def billingclient_version = "3.0.0" def javamail_version = "1.6.5" def jsoup_version = "1.13.1" def css_version = "0.9.27" @@ -276,7 +275,7 @@ dependencies { def biweekly_version = "0.6.3" def photoview_version = "2.3.0" def relinker_version = "1.3.1" - def markwon_version = "4.3.1" + def markwon_version = "4.4.0" def bouncycastle_version = "1.65" def colorpicker_version = "0.0.15" def appauth_version = "0.7.1" @@ -357,13 +356,15 @@ dependencies { //implementation "androidx.textclassifier:textclassifier:$textclassifier_version" // https://developer.android.com/google/play/billing/billing_library_releases_notes - implementation "com.android.billingclient:billing:$billingclient_version" + // https://android-developers.googleblog.com/2020/06/meet-google-play-billing-library.html + githubImplementation "com.android.billingclient:billing:$billingclient_version" + playImplementation "com.android.billingclient:billing:$billingclient_version" // https://javaee.github.io/javamail/ // https://projects.eclipse.org/projects/ee4j.javamail // https://mvnrepository.com/artifact/com.sun.mail - implementation "com.sun.mail:android-mail:$javamail_version" - implementation "com.sun.mail:android-activation:$javamail_version" + //implementation "com.sun.mail:android-mail:$javamail_version" + //implementation "com.sun.mail:android-activation:$javamail_version" // https://jsoup.org/news/ implementation "org.jsoup:jsoup:$jsoup_version" diff --git a/app/schemas/eu.faircode.email.DB/163.json b/app/schemas/eu.faircode.email.DB/163.json new file mode 100644 index 0000000000..126a0ce01c --- /dev/null +++ b/app/schemas/eu.faircode.email.DB/163.json @@ -0,0 +1,2255 @@ +{ + "formatVersion": 1, + "database": { + "version": 163, + "identityHash": "0f6294f5de89616db4a67550990e237c", + "entities": [ + { + "tableName": "identity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `account` INTEGER NOT NULL, `display` TEXT, `color` INTEGER, `signature` TEXT, `host` TEXT NOT NULL, `starttls` INTEGER NOT NULL, `insecure` INTEGER NOT NULL, `port` INTEGER NOT NULL, `auth_type` INTEGER NOT NULL, `provider` TEXT, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `certificate` INTEGER NOT NULL, `certificate_alias` TEXT, `realm` TEXT, `fingerprint` TEXT, `use_ip` INTEGER NOT NULL, `ehlo` TEXT, `synchronize` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `sender_extra` INTEGER NOT NULL, `sender_extra_regex` TEXT, `replyto` TEXT, `cc` TEXT, `bcc` TEXT, `unicode` INTEGER NOT NULL, `plain_only` INTEGER NOT NULL, `encrypt` INTEGER NOT NULL, `delivery_receipt` INTEGER NOT NULL, `read_receipt` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `sent_folder` INTEGER, `sign_key` INTEGER, `sign_key_alias` TEXT, `tbd` INTEGER, `state` TEXT, `error` TEXT, `last_connected` INTEGER, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "display", + "columnName": "display", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starttls", + "columnName": "starttls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insecure", + "columnName": "insecure", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificate", + "columnName": "certificate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "certificate_alias", + "columnName": "certificate_alias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "realm", + "columnName": "realm", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "use_ip", + "columnName": "use_ip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ehlo", + "columnName": "ehlo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender_extra", + "columnName": "sender_extra", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender_extra_regex", + "columnName": "sender_extra_regex", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyto", + "columnName": "replyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cc", + "columnName": "cc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bcc", + "columnName": "bcc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unicode", + "columnName": "unicode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plain_only", + "columnName": "plain_only", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encrypt", + "columnName": "encrypt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delivery_receipt", + "columnName": "delivery_receipt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read_receipt", + "columnName": "read_receipt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "store_sent", + "columnName": "store_sent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sent_folder", + "columnName": "sent_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sign_key", + "columnName": "sign_key", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sign_key_alias", + "columnName": "sign_key_alias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_connected", + "columnName": "last_connected", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_identity_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_identity_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_identity_account_email", + "unique": false, + "columnNames": [ + "account", + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_identity_account_email` ON `${TABLE_NAME}` (`account`, `email`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`order` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `pop` INTEGER NOT NULL, `host` TEXT NOT NULL, `starttls` INTEGER NOT NULL, `insecure` INTEGER NOT NULL, `port` INTEGER NOT NULL, `auth_type` INTEGER NOT NULL, `provider` TEXT, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `certificate` INTEGER NOT NULL, `certificate_alias` TEXT, `realm` TEXT, `fingerprint` TEXT, `name` TEXT, `signature` TEXT, `color` INTEGER, `synchronize` INTEGER NOT NULL, `ondemand` INTEGER NOT NULL, `poll_exempted` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `notify` INTEGER NOT NULL, `browse` INTEGER NOT NULL, `leave_on_server` INTEGER NOT NULL, `leave_deleted` INTEGER NOT NULL, `leave_on_device` INTEGER NOT NULL, `max_messages` INTEGER, `auto_seen` INTEGER NOT NULL, `separator` INTEGER, `swipe_left` INTEGER, `swipe_right` INTEGER, `move_to` INTEGER, `poll_interval` INTEGER NOT NULL, `keep_alive_ok` INTEGER NOT NULL, `keep_alive_failed` INTEGER NOT NULL, `keep_alive_succeeded` INTEGER NOT NULL, `partial_fetch` INTEGER NOT NULL, `ignore_size` INTEGER NOT NULL, `use_date` INTEGER NOT NULL, `prefix` TEXT, `quota_usage` INTEGER, `quota_limit` INTEGER, `created` INTEGER, `tbd` INTEGER, `thread` INTEGER, `state` TEXT, `warning` TEXT, `error` TEXT, `last_connected` INTEGER)", + "fields": [ + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "protocol", + "columnName": "pop", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starttls", + "columnName": "starttls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insecure", + "columnName": "insecure", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificate", + "columnName": "certificate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "certificate_alias", + "columnName": "certificate_alias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "realm", + "columnName": "realm", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ondemand", + "columnName": "ondemand", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll_exempted", + "columnName": "poll_exempted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notify", + "columnName": "notify", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "browse", + "columnName": "browse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "leave_on_server", + "columnName": "leave_on_server", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "leave_deleted", + "columnName": "leave_deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "leave_on_device", + "columnName": "leave_on_device", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "max_messages", + "columnName": "max_messages", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "auto_seen", + "columnName": "auto_seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "separator", + "columnName": "separator", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swipe_left", + "columnName": "swipe_left", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swipe_right", + "columnName": "swipe_right", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "move_to", + "columnName": "move_to", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll_interval", + "columnName": "poll_interval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep_alive_ok", + "columnName": "keep_alive_ok", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep_alive_failed", + "columnName": "keep_alive_failed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep_alive_succeeded", + "columnName": "keep_alive_succeeded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partial_fetch", + "columnName": "partial_fetch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignore_size", + "columnName": "ignore_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "use_date", + "columnName": "use_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prefix", + "columnName": "prefix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quota_usage", + "columnName": "quota_usage", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quota_limit", + "columnName": "quota_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "thread", + "columnName": "thread", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "warning", + "columnName": "warning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_connected", + "columnName": "last_connected", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "folder", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`order` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `parent` INTEGER, `uidv` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `level` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `poll` INTEGER NOT NULL, `poll_factor` INTEGER NOT NULL, `poll_count` INTEGER NOT NULL, `download` INTEGER NOT NULL, `subscribed` INTEGER, `sync_days` INTEGER NOT NULL, `keep_days` INTEGER NOT NULL, `auto_delete` INTEGER NOT NULL, `display` TEXT, `color` INTEGER, `hide` INTEGER NOT NULL, `collapsed` INTEGER NOT NULL, `unified` INTEGER NOT NULL, `navigation` INTEGER NOT NULL, `notify` INTEGER NOT NULL, `total` INTEGER, `keywords` TEXT, `initialize` INTEGER NOT NULL, `tbc` INTEGER, `tbd` INTEGER, `rename` TEXT, `state` TEXT, `sync_state` TEXT, `read_only` INTEGER NOT NULL, `selectable` INTEGER NOT NULL, `inferiors` INTEGER NOT NULL, `error` TEXT, `last_sync` INTEGER, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uidv", + "columnName": "uidv", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll_factor", + "columnName": "poll_factor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll_count", + "columnName": "poll_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "download", + "columnName": "download", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sync_days", + "columnName": "sync_days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep_days", + "columnName": "keep_days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "auto_delete", + "columnName": "auto_delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "display", + "columnName": "display", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hide", + "columnName": "hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collapsed", + "columnName": "collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unified", + "columnName": "unified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "navigation", + "columnName": "navigation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notify", + "columnName": "notify", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "total", + "columnName": "total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "initialize", + "columnName": "initialize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tbc", + "columnName": "tbc", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "rename", + "columnName": "rename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync_state", + "columnName": "sync_state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read_only", + "columnName": "read_only", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectable", + "columnName": "selectable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inferiors", + "columnName": "inferiors", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_sync", + "columnName": "last_sync", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_folder_account_name", + "unique": true, + "columnNames": [ + "account", + "name" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)" + }, + { + "name": "index_folder_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_folder_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_folder_type", + "unique": false, + "columnNames": [ + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_type` ON `${TABLE_NAME}` (`type`)" + }, + { + "name": "index_folder_unified", + "unique": false, + "columnNames": [ + "unified" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_unified` ON `${TABLE_NAME}` (`unified`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "message", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER NOT NULL, `folder` INTEGER NOT NULL, `identity` INTEGER, `extra` TEXT, `replying` INTEGER, `forwarding` INTEGER, `uid` INTEGER, `uidl` TEXT, `msgid` TEXT, `hash` TEXT, `references` TEXT, `deliveredto` TEXT, `inreplyto` TEXT, `wasforwardedfrom` TEXT, `thread` TEXT, `priority` INTEGER, `importance` INTEGER, `receipt` INTEGER, `receipt_request` INTEGER, `receipt_to` TEXT, `dkim` INTEGER, `spf` INTEGER, `dmarc` INTEGER, `mx` INTEGER, `avatar` TEXT, `sender` TEXT, `submitter` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `list_post` TEXT, `unsubscribe` TEXT, `autocrypt` TEXT, `headers` TEXT, `raw` INTEGER, `subject` TEXT, `size` INTEGER, `total` INTEGER, `attachments` INTEGER NOT NULL, `content` INTEGER NOT NULL, `language` TEXT, `plain_only` INTEGER, `encrypt` INTEGER, `ui_encrypt` INTEGER, `verified` INTEGER NOT NULL, `preview` TEXT, `signature` INTEGER NOT NULL, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `answered` INTEGER NOT NULL, `flagged` INTEGER NOT NULL, `flags` TEXT, `keywords` TEXT, `notifying` INTEGER NOT NULL, `fts` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_answered` INTEGER NOT NULL, `ui_flagged` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `ui_ignored` INTEGER NOT NULL, `ui_browsed` INTEGER NOT NULL, `ui_busy` INTEGER, `ui_snoozed` INTEGER, `ui_unsnoozed` INTEGER NOT NULL, `color` INTEGER, `revision` INTEGER, `revisions` INTEGER, `warning` TEXT, `error` TEXT, `last_attempt` INTEGER, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`forwarding`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identity", + "columnName": "identity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "extra", + "columnName": "extra", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replying", + "columnName": "replying", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forwarding", + "columnName": "forwarding", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uidl", + "columnName": "uidl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "msgid", + "columnName": "msgid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "references", + "columnName": "references", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deliveredto", + "columnName": "deliveredto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inreplyto", + "columnName": "inreplyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wasforwardedfrom", + "columnName": "wasforwardedfrom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thread", + "columnName": "thread", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "importance", + "columnName": "importance", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "receipt", + "columnName": "receipt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "receipt_request", + "columnName": "receipt_request", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "receipt_to", + "columnName": "receipt_to", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dkim", + "columnName": "dkim", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "spf", + "columnName": "spf", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dmarc", + "columnName": "dmarc", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mx", + "columnName": "mx", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submitter", + "columnName": "submitter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "from", + "columnName": "from", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cc", + "columnName": "cc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bcc", + "columnName": "bcc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "list_post", + "columnName": "list_post", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unsubscribe", + "columnName": "unsubscribe", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "autocrypt", + "columnName": "autocrypt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "headers", + "columnName": "headers", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "raw", + "columnName": "raw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "total", + "columnName": "total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plain_only", + "columnName": "plain_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "encrypt", + "columnName": "encrypt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ui_encrypt", + "columnName": "ui_encrypt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "verified", + "columnName": "verified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "preview", + "columnName": "preview", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sent", + "columnName": "sent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stored", + "columnName": "stored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "seen", + "columnName": "seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "answered", + "columnName": "answered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flagged", + "columnName": "flagged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notifying", + "columnName": "notifying", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fts", + "columnName": "fts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_seen", + "columnName": "ui_seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_answered", + "columnName": "ui_answered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_flagged", + "columnName": "ui_flagged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_hide", + "columnName": "ui_hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_found", + "columnName": "ui_found", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_ignored", + "columnName": "ui_ignored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_browsed", + "columnName": "ui_browsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_busy", + "columnName": "ui_busy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ui_snoozed", + "columnName": "ui_snoozed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ui_unsnoozed", + "columnName": "ui_unsnoozed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "revision", + "columnName": "revision", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "revisions", + "columnName": "revisions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "warning", + "columnName": "warning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_attempt", + "columnName": "last_attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_message_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_message_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_message_identity", + "unique": false, + "columnNames": [ + "identity" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_identity` ON `${TABLE_NAME}` (`identity`)" + }, + { + "name": "index_message_folder_uid", + "unique": true, + "columnNames": [ + "folder", + "uid" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)" + }, + { + "name": "index_message_inreplyto", + "unique": false, + "columnNames": [ + "inreplyto" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_inreplyto` ON `${TABLE_NAME}` (`inreplyto`)" + }, + { + "name": "index_message_msgid", + "unique": false, + "columnNames": [ + "msgid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_msgid` ON `${TABLE_NAME}` (`msgid`)" + }, + { + "name": "index_message_thread", + "unique": false, + "columnNames": [ + "thread" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_thread` ON `${TABLE_NAME}` (`thread`)" + }, + { + "name": "index_message_sender", + "unique": false, + "columnNames": [ + "sender" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_sender` ON `${TABLE_NAME}` (`sender`)" + }, + { + "name": "index_message_received", + "unique": false, + "columnNames": [ + "received" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_received` ON `${TABLE_NAME}` (`received`)" + }, + { + "name": "index_message_subject", + "unique": false, + "columnNames": [ + "subject" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_subject` ON `${TABLE_NAME}` (`subject`)" + }, + { + "name": "index_message_ui_seen", + "unique": false, + "columnNames": [ + "ui_seen" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)" + }, + { + "name": "index_message_ui_flagged", + "unique": false, + "columnNames": [ + "ui_flagged" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_flagged` ON `${TABLE_NAME}` (`ui_flagged`)" + }, + { + "name": "index_message_ui_hide", + "unique": false, + "columnNames": [ + "ui_hide" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)" + }, + { + "name": "index_message_ui_found", + "unique": false, + "columnNames": [ + "ui_found" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_found` ON `${TABLE_NAME}` (`ui_found`)" + }, + { + "name": "index_message_ui_ignored", + "unique": false, + "columnNames": [ + "ui_ignored" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_ignored` ON `${TABLE_NAME}` (`ui_ignored`)" + }, + { + "name": "index_message_ui_browsed", + "unique": false, + "columnNames": [ + "ui_browsed" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_browsed` ON `${TABLE_NAME}` (`ui_browsed`)" + }, + { + "name": "index_message_ui_snoozed", + "unique": false, + "columnNames": [ + "ui_snoozed" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_snoozed` ON `${TABLE_NAME}` (`ui_snoozed`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "identity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "identity" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "replying" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "forwarding" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "attachment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `disposition` TEXT, `cid` TEXT, `encryption` INTEGER, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sequence", + "columnName": "sequence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disposition", + "columnName": "disposition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cid", + "columnName": "cid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryption", + "columnName": "encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "available", + "columnName": "available", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_attachment_message", + "unique": false, + "columnNames": [ + "message" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attachment_message` ON `${TABLE_NAME}` (`message`)" + }, + { + "name": "index_attachment_message_sequence", + "unique": true, + "columnNames": [ + "message", + "sequence" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)" + }, + { + "name": "index_attachment_message_cid", + "unique": false, + "columnNames": [ + "message", + "cid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attachment_message_cid` ON `${TABLE_NAME}` (`message`, `cid`)" + } + ], + "foreignKeys": [ + { + "table": "message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "message" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "operation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `message` INTEGER, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, `tries` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "args", + "columnName": "args", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tries", + "columnName": "tries", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_operation_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_operation_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_operation_message", + "unique": false, + "columnNames": [ + "message" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_message` ON `${TABLE_NAME}` (`message`)" + }, + { + "name": "index_operation_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_operation_state", + "unique": false, + "columnNames": [ + "state" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_state` ON `${TABLE_NAME}` (`state`)" + } + ], + "foreignKeys": [ + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "message" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "contact", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER NOT NULL, `type` INTEGER NOT NULL, `email` TEXT NOT NULL, `name` TEXT, `avatar` TEXT, `times_contacted` INTEGER NOT NULL, `first_contacted` INTEGER NOT NULL, `last_contacted` INTEGER NOT NULL, `state` INTEGER NOT NULL, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "times_contacted", + "columnName": "times_contacted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "first_contacted", + "columnName": "first_contacted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "last_contacted", + "columnName": "last_contacted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_contact_account_type_email", + "unique": true, + "columnNames": [ + "account", + "type", + "email" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_contact_account_type_email` ON `${TABLE_NAME}` (`account`, `type`, `email`)" + }, + { + "name": "index_contact_email", + "unique": false, + "columnNames": [ + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_email` ON `${TABLE_NAME}` (`email`)" + }, + { + "name": "index_contact_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_contact_avatar", + "unique": false, + "columnNames": [ + "avatar" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_avatar` ON `${TABLE_NAME}` (`avatar`)" + }, + { + "name": "index_contact_times_contacted", + "unique": false, + "columnNames": [ + "times_contacted" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_times_contacted` ON `${TABLE_NAME}` (`times_contacted`)" + }, + { + "name": "index_contact_last_contacted", + "unique": false, + "columnNames": [ + "last_contacted" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_last_contacted` ON `${TABLE_NAME}` (`last_contacted`)" + }, + { + "name": "index_contact_state", + "unique": false, + "columnNames": [ + "state" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_state` ON `${TABLE_NAME}` (`state`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "certificate", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `fingerprint` TEXT NOT NULL, `intermediate` INTEGER NOT NULL, `email` TEXT NOT NULL, `subject` TEXT, `after` INTEGER, `before` INTEGER, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "intermediate", + "columnName": "intermediate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "after", + "columnName": "after", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "before", + "columnName": "before", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_certificate_fingerprint_email", + "unique": true, + "columnNames": [ + "fingerprint", + "email" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_certificate_fingerprint_email` ON `${TABLE_NAME}` (`fingerprint`, `email`)" + }, + { + "name": "index_certificate_email", + "unique": false, + "columnNames": [ + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_certificate_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "answer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `favorite` INTEGER NOT NULL, `hide` INTEGER NOT NULL, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hide", + "columnName": "hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rule", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `stop` INTEGER NOT NULL, `condition` TEXT NOT NULL, `action` TEXT NOT NULL, `applied` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stop", + "columnName": "stop", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "applied", + "columnName": "applied", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_rule_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rule_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_rule_order", + "unique": false, + "columnNames": [ + "order" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rule_order` ON `${TABLE_NAME}` (`order`)" + } + ], + "foreignKeys": [ + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_log_time", + "unique": false, + "columnNames": [ + "time" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + } + ], + "views": [ + { + "viewName": "account_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT id, pop, name, color, synchronize, notify, auto_seen, created FROM account" + }, + { + "viewName": "identity_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT id, name, email, display, color, synchronize FROM identity" + }, + { + "viewName": "folder_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT id, account, name, type, display, color, unified, notify, read_only FROM folder" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0f6294f5de89616db4a67550990e237c')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/eu.faircode.email.DB/164.json b/app/schemas/eu.faircode.email.DB/164.json new file mode 100644 index 0000000000..6f48ce9402 --- /dev/null +++ b/app/schemas/eu.faircode.email.DB/164.json @@ -0,0 +1,2255 @@ +{ + "formatVersion": 1, + "database": { + "version": 164, + "identityHash": "0f6294f5de89616db4a67550990e237c", + "entities": [ + { + "tableName": "identity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `email` TEXT NOT NULL, `account` INTEGER NOT NULL, `display` TEXT, `color` INTEGER, `signature` TEXT, `host` TEXT NOT NULL, `starttls` INTEGER NOT NULL, `insecure` INTEGER NOT NULL, `port` INTEGER NOT NULL, `auth_type` INTEGER NOT NULL, `provider` TEXT, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `certificate` INTEGER NOT NULL, `certificate_alias` TEXT, `realm` TEXT, `fingerprint` TEXT, `use_ip` INTEGER NOT NULL, `ehlo` TEXT, `synchronize` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `sender_extra` INTEGER NOT NULL, `sender_extra_regex` TEXT, `replyto` TEXT, `cc` TEXT, `bcc` TEXT, `unicode` INTEGER NOT NULL, `plain_only` INTEGER NOT NULL, `encrypt` INTEGER NOT NULL, `delivery_receipt` INTEGER NOT NULL, `read_receipt` INTEGER NOT NULL, `store_sent` INTEGER NOT NULL, `sent_folder` INTEGER, `sign_key` INTEGER, `sign_key_alias` TEXT, `tbd` INTEGER, `state` TEXT, `error` TEXT, `last_connected` INTEGER, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "display", + "columnName": "display", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starttls", + "columnName": "starttls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insecure", + "columnName": "insecure", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificate", + "columnName": "certificate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "certificate_alias", + "columnName": "certificate_alias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "realm", + "columnName": "realm", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "use_ip", + "columnName": "use_ip", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ehlo", + "columnName": "ehlo", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender_extra", + "columnName": "sender_extra", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sender_extra_regex", + "columnName": "sender_extra_regex", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replyto", + "columnName": "replyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cc", + "columnName": "cc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bcc", + "columnName": "bcc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unicode", + "columnName": "unicode", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "plain_only", + "columnName": "plain_only", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "encrypt", + "columnName": "encrypt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "delivery_receipt", + "columnName": "delivery_receipt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "read_receipt", + "columnName": "read_receipt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "store_sent", + "columnName": "store_sent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sent_folder", + "columnName": "sent_folder", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sign_key", + "columnName": "sign_key", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sign_key_alias", + "columnName": "sign_key_alias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_connected", + "columnName": "last_connected", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_identity_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_identity_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_identity_account_email", + "unique": false, + "columnNames": [ + "account", + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_identity_account_email` ON `${TABLE_NAME}` (`account`, `email`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "account", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`order` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `pop` INTEGER NOT NULL, `host` TEXT NOT NULL, `starttls` INTEGER NOT NULL, `insecure` INTEGER NOT NULL, `port` INTEGER NOT NULL, `auth_type` INTEGER NOT NULL, `provider` TEXT, `user` TEXT NOT NULL, `password` TEXT NOT NULL, `certificate` INTEGER NOT NULL, `certificate_alias` TEXT, `realm` TEXT, `fingerprint` TEXT, `name` TEXT, `signature` TEXT, `color` INTEGER, `synchronize` INTEGER NOT NULL, `ondemand` INTEGER NOT NULL, `poll_exempted` INTEGER NOT NULL, `primary` INTEGER NOT NULL, `notify` INTEGER NOT NULL, `browse` INTEGER NOT NULL, `leave_on_server` INTEGER NOT NULL, `leave_deleted` INTEGER NOT NULL, `leave_on_device` INTEGER NOT NULL, `max_messages` INTEGER, `auto_seen` INTEGER NOT NULL, `separator` INTEGER, `swipe_left` INTEGER, `swipe_right` INTEGER, `move_to` INTEGER, `poll_interval` INTEGER NOT NULL, `keep_alive_ok` INTEGER NOT NULL, `keep_alive_failed` INTEGER NOT NULL, `keep_alive_succeeded` INTEGER NOT NULL, `partial_fetch` INTEGER NOT NULL, `ignore_size` INTEGER NOT NULL, `use_date` INTEGER NOT NULL, `prefix` TEXT, `quota_usage` INTEGER, `quota_limit` INTEGER, `created` INTEGER, `tbd` INTEGER, `thread` INTEGER, `state` TEXT, `warning` TEXT, `error` TEXT, `last_connected` INTEGER)", + "fields": [ + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "protocol", + "columnName": "pop", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "host", + "columnName": "host", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "starttls", + "columnName": "starttls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "insecure", + "columnName": "insecure", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "port", + "columnName": "port", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "auth_type", + "columnName": "auth_type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "provider", + "columnName": "provider", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "user", + "columnName": "user", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "password", + "columnName": "password", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "certificate", + "columnName": "certificate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "certificate_alias", + "columnName": "certificate_alias", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "realm", + "columnName": "realm", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ondemand", + "columnName": "ondemand", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll_exempted", + "columnName": "poll_exempted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "primary", + "columnName": "primary", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notify", + "columnName": "notify", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "browse", + "columnName": "browse", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "leave_on_server", + "columnName": "leave_on_server", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "leave_deleted", + "columnName": "leave_deleted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "leave_on_device", + "columnName": "leave_on_device", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "max_messages", + "columnName": "max_messages", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "auto_seen", + "columnName": "auto_seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "separator", + "columnName": "separator", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swipe_left", + "columnName": "swipe_left", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "swipe_right", + "columnName": "swipe_right", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "move_to", + "columnName": "move_to", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll_interval", + "columnName": "poll_interval", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep_alive_ok", + "columnName": "keep_alive_ok", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep_alive_failed", + "columnName": "keep_alive_failed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep_alive_succeeded", + "columnName": "keep_alive_succeeded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "partial_fetch", + "columnName": "partial_fetch", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ignore_size", + "columnName": "ignore_size", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "use_date", + "columnName": "use_date", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prefix", + "columnName": "prefix", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "quota_usage", + "columnName": "quota_usage", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "quota_limit", + "columnName": "quota_limit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "thread", + "columnName": "thread", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "warning", + "columnName": "warning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_connected", + "columnName": "last_connected", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "folder", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`order` INTEGER, `id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `parent` INTEGER, `uidv` INTEGER, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `level` INTEGER NOT NULL, `synchronize` INTEGER NOT NULL, `poll` INTEGER NOT NULL, `poll_factor` INTEGER NOT NULL, `poll_count` INTEGER NOT NULL, `download` INTEGER NOT NULL, `subscribed` INTEGER, `sync_days` INTEGER NOT NULL, `keep_days` INTEGER NOT NULL, `auto_delete` INTEGER NOT NULL, `display` TEXT, `color` INTEGER, `hide` INTEGER NOT NULL, `collapsed` INTEGER NOT NULL, `unified` INTEGER NOT NULL, `navigation` INTEGER NOT NULL, `notify` INTEGER NOT NULL, `total` INTEGER, `keywords` TEXT, `initialize` INTEGER NOT NULL, `tbc` INTEGER, `tbd` INTEGER, `rename` TEXT, `state` TEXT, `sync_state` TEXT, `read_only` INTEGER NOT NULL, `selectable` INTEGER NOT NULL, `inferiors` INTEGER NOT NULL, `error` TEXT, `last_sync` INTEGER, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uidv", + "columnName": "uidv", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "level", + "columnName": "level", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "synchronize", + "columnName": "synchronize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll_factor", + "columnName": "poll_factor", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "poll_count", + "columnName": "poll_count", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "download", + "columnName": "download", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "subscribed", + "columnName": "subscribed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "sync_days", + "columnName": "sync_days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "keep_days", + "columnName": "keep_days", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "auto_delete", + "columnName": "auto_delete", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "display", + "columnName": "display", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "hide", + "columnName": "hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collapsed", + "columnName": "collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "unified", + "columnName": "unified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "navigation", + "columnName": "navigation", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notify", + "columnName": "notify", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "total", + "columnName": "total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "initialize", + "columnName": "initialize", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tbc", + "columnName": "tbc", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "tbd", + "columnName": "tbd", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "rename", + "columnName": "rename", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sync_state", + "columnName": "sync_state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "read_only", + "columnName": "read_only", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selectable", + "columnName": "selectable", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inferiors", + "columnName": "inferiors", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_sync", + "columnName": "last_sync", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_folder_account_name", + "unique": true, + "columnNames": [ + "account", + "name" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_folder_account_name` ON `${TABLE_NAME}` (`account`, `name`)" + }, + { + "name": "index_folder_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_folder_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_folder_type", + "unique": false, + "columnNames": [ + "type" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_type` ON `${TABLE_NAME}` (`type`)" + }, + { + "name": "index_folder_unified", + "unique": false, + "columnNames": [ + "unified" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_folder_unified` ON `${TABLE_NAME}` (`unified`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "message", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER NOT NULL, `folder` INTEGER NOT NULL, `identity` INTEGER, `extra` TEXT, `replying` INTEGER, `forwarding` INTEGER, `uid` INTEGER, `uidl` TEXT, `msgid` TEXT, `hash` TEXT, `references` TEXT, `deliveredto` TEXT, `inreplyto` TEXT, `wasforwardedfrom` TEXT, `thread` TEXT, `priority` INTEGER, `importance` INTEGER, `receipt` INTEGER, `receipt_request` INTEGER, `receipt_to` TEXT, `dkim` INTEGER, `spf` INTEGER, `dmarc` INTEGER, `mx` INTEGER, `avatar` TEXT, `sender` TEXT, `submitter` TEXT, `from` TEXT, `to` TEXT, `cc` TEXT, `bcc` TEXT, `reply` TEXT, `list_post` TEXT, `unsubscribe` TEXT, `autocrypt` TEXT, `headers` TEXT, `raw` INTEGER, `subject` TEXT, `size` INTEGER, `total` INTEGER, `attachments` INTEGER NOT NULL, `content` INTEGER NOT NULL, `language` TEXT, `plain_only` INTEGER, `encrypt` INTEGER, `ui_encrypt` INTEGER, `verified` INTEGER NOT NULL, `preview` TEXT, `signature` INTEGER NOT NULL, `sent` INTEGER, `received` INTEGER NOT NULL, `stored` INTEGER NOT NULL, `seen` INTEGER NOT NULL, `answered` INTEGER NOT NULL, `flagged` INTEGER NOT NULL, `flags` TEXT, `keywords` TEXT, `notifying` INTEGER NOT NULL, `fts` INTEGER NOT NULL, `ui_seen` INTEGER NOT NULL, `ui_answered` INTEGER NOT NULL, `ui_flagged` INTEGER NOT NULL, `ui_hide` INTEGER NOT NULL, `ui_found` INTEGER NOT NULL, `ui_ignored` INTEGER NOT NULL, `ui_browsed` INTEGER NOT NULL, `ui_busy` INTEGER, `ui_snoozed` INTEGER, `ui_unsnoozed` INTEGER NOT NULL, `color` INTEGER, `revision` INTEGER, `revisions` INTEGER, `warning` TEXT, `error` TEXT, `last_attempt` INTEGER, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`identity`) REFERENCES `identity`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`replying`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL , FOREIGN KEY(`forwarding`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE SET NULL )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "identity", + "columnName": "identity", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "extra", + "columnName": "extra", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "replying", + "columnName": "replying", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "forwarding", + "columnName": "forwarding", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "uidl", + "columnName": "uidl", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "msgid", + "columnName": "msgid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "hash", + "columnName": "hash", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "references", + "columnName": "references", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "deliveredto", + "columnName": "deliveredto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inreplyto", + "columnName": "inreplyto", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "wasforwardedfrom", + "columnName": "wasforwardedfrom", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "thread", + "columnName": "thread", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "priority", + "columnName": "priority", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "importance", + "columnName": "importance", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "receipt", + "columnName": "receipt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "receipt_request", + "columnName": "receipt_request", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "receipt_to", + "columnName": "receipt_to", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "dkim", + "columnName": "dkim", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "spf", + "columnName": "spf", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dmarc", + "columnName": "dmarc", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "mx", + "columnName": "mx", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sender", + "columnName": "sender", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "submitter", + "columnName": "submitter", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "from", + "columnName": "from", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "to", + "columnName": "to", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cc", + "columnName": "cc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "bcc", + "columnName": "bcc", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reply", + "columnName": "reply", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "list_post", + "columnName": "list_post", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "unsubscribe", + "columnName": "unsubscribe", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "autocrypt", + "columnName": "autocrypt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "headers", + "columnName": "headers", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "raw", + "columnName": "raw", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "total", + "columnName": "total", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "plain_only", + "columnName": "plain_only", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "encrypt", + "columnName": "encrypt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ui_encrypt", + "columnName": "ui_encrypt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "verified", + "columnName": "verified", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "preview", + "columnName": "preview", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "signature", + "columnName": "signature", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sent", + "columnName": "sent", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "received", + "columnName": "received", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stored", + "columnName": "stored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "seen", + "columnName": "seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "answered", + "columnName": "answered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flagged", + "columnName": "flagged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "flags", + "columnName": "flags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "keywords", + "columnName": "keywords", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notifying", + "columnName": "notifying", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "fts", + "columnName": "fts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_seen", + "columnName": "ui_seen", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_answered", + "columnName": "ui_answered", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_flagged", + "columnName": "ui_flagged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_hide", + "columnName": "ui_hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_found", + "columnName": "ui_found", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_ignored", + "columnName": "ui_ignored", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_browsed", + "columnName": "ui_browsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "ui_busy", + "columnName": "ui_busy", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ui_snoozed", + "columnName": "ui_snoozed", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "ui_unsnoozed", + "columnName": "ui_unsnoozed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "color", + "columnName": "color", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "revision", + "columnName": "revision", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "revisions", + "columnName": "revisions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "warning", + "columnName": "warning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "last_attempt", + "columnName": "last_attempt", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_message_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_message_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_message_identity", + "unique": false, + "columnNames": [ + "identity" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_identity` ON `${TABLE_NAME}` (`identity`)" + }, + { + "name": "index_message_folder_uid", + "unique": true, + "columnNames": [ + "folder", + "uid" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_message_folder_uid` ON `${TABLE_NAME}` (`folder`, `uid`)" + }, + { + "name": "index_message_inreplyto", + "unique": false, + "columnNames": [ + "inreplyto" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_inreplyto` ON `${TABLE_NAME}` (`inreplyto`)" + }, + { + "name": "index_message_msgid", + "unique": false, + "columnNames": [ + "msgid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_msgid` ON `${TABLE_NAME}` (`msgid`)" + }, + { + "name": "index_message_thread", + "unique": false, + "columnNames": [ + "thread" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_thread` ON `${TABLE_NAME}` (`thread`)" + }, + { + "name": "index_message_sender", + "unique": false, + "columnNames": [ + "sender" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_sender` ON `${TABLE_NAME}` (`sender`)" + }, + { + "name": "index_message_received", + "unique": false, + "columnNames": [ + "received" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_received` ON `${TABLE_NAME}` (`received`)" + }, + { + "name": "index_message_subject", + "unique": false, + "columnNames": [ + "subject" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_subject` ON `${TABLE_NAME}` (`subject`)" + }, + { + "name": "index_message_ui_seen", + "unique": false, + "columnNames": [ + "ui_seen" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_seen` ON `${TABLE_NAME}` (`ui_seen`)" + }, + { + "name": "index_message_ui_flagged", + "unique": false, + "columnNames": [ + "ui_flagged" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_flagged` ON `${TABLE_NAME}` (`ui_flagged`)" + }, + { + "name": "index_message_ui_hide", + "unique": false, + "columnNames": [ + "ui_hide" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_hide` ON `${TABLE_NAME}` (`ui_hide`)" + }, + { + "name": "index_message_ui_found", + "unique": false, + "columnNames": [ + "ui_found" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_found` ON `${TABLE_NAME}` (`ui_found`)" + }, + { + "name": "index_message_ui_ignored", + "unique": false, + "columnNames": [ + "ui_ignored" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_ignored` ON `${TABLE_NAME}` (`ui_ignored`)" + }, + { + "name": "index_message_ui_browsed", + "unique": false, + "columnNames": [ + "ui_browsed" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_browsed` ON `${TABLE_NAME}` (`ui_browsed`)" + }, + { + "name": "index_message_ui_snoozed", + "unique": false, + "columnNames": [ + "ui_snoozed" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_message_ui_snoozed` ON `${TABLE_NAME}` (`ui_snoozed`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "identity", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "identity" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "replying" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "SET NULL", + "onUpdate": "NO ACTION", + "columns": [ + "forwarding" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "attachment", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `message` INTEGER NOT NULL, `sequence` INTEGER NOT NULL, `name` TEXT, `type` TEXT NOT NULL, `disposition` TEXT, `cid` TEXT, `encryption` INTEGER, `size` INTEGER, `progress` INTEGER, `available` INTEGER NOT NULL, `error` TEXT, FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sequence", + "columnName": "sequence", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disposition", + "columnName": "disposition", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "cid", + "columnName": "cid", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "encryption", + "columnName": "encryption", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "size", + "columnName": "size", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "available", + "columnName": "available", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_attachment_message", + "unique": false, + "columnNames": [ + "message" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attachment_message` ON `${TABLE_NAME}` (`message`)" + }, + { + "name": "index_attachment_message_sequence", + "unique": true, + "columnNames": [ + "message", + "sequence" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_attachment_message_sequence` ON `${TABLE_NAME}` (`message`, `sequence`)" + }, + { + "name": "index_attachment_message_cid", + "unique": false, + "columnNames": [ + "message", + "cid" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_attachment_message_cid` ON `${TABLE_NAME}` (`message`, `cid`)" + } + ], + "foreignKeys": [ + { + "table": "message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "message" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "operation", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER, `folder` INTEGER NOT NULL, `message` INTEGER, `name` TEXT NOT NULL, `args` TEXT NOT NULL, `created` INTEGER NOT NULL, `tries` INTEGER NOT NULL, `state` TEXT, `error` TEXT, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`message`) REFERENCES `message`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "message", + "columnName": "message", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "args", + "columnName": "args", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "created", + "columnName": "created", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "tries", + "columnName": "tries", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "error", + "columnName": "error", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_operation_account", + "unique": false, + "columnNames": [ + "account" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_account` ON `${TABLE_NAME}` (`account`)" + }, + { + "name": "index_operation_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_operation_message", + "unique": false, + "columnNames": [ + "message" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_message` ON `${TABLE_NAME}` (`message`)" + }, + { + "name": "index_operation_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_operation_state", + "unique": false, + "columnNames": [ + "state" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_operation_state` ON `${TABLE_NAME}` (`state`)" + } + ], + "foreignKeys": [ + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + }, + { + "table": "message", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "message" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "contact", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `account` INTEGER NOT NULL, `type` INTEGER NOT NULL, `email` TEXT NOT NULL, `name` TEXT, `avatar` TEXT, `times_contacted` INTEGER NOT NULL, `first_contacted` INTEGER NOT NULL, `last_contacted` INTEGER NOT NULL, `state` INTEGER NOT NULL, FOREIGN KEY(`account`) REFERENCES `account`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "times_contacted", + "columnName": "times_contacted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "first_contacted", + "columnName": "first_contacted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "last_contacted", + "columnName": "last_contacted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "state", + "columnName": "state", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_contact_account_type_email", + "unique": true, + "columnNames": [ + "account", + "type", + "email" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_contact_account_type_email` ON `${TABLE_NAME}` (`account`, `type`, `email`)" + }, + { + "name": "index_contact_email", + "unique": false, + "columnNames": [ + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_email` ON `${TABLE_NAME}` (`email`)" + }, + { + "name": "index_contact_name", + "unique": false, + "columnNames": [ + "name" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_name` ON `${TABLE_NAME}` (`name`)" + }, + { + "name": "index_contact_avatar", + "unique": false, + "columnNames": [ + "avatar" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_avatar` ON `${TABLE_NAME}` (`avatar`)" + }, + { + "name": "index_contact_times_contacted", + "unique": false, + "columnNames": [ + "times_contacted" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_times_contacted` ON `${TABLE_NAME}` (`times_contacted`)" + }, + { + "name": "index_contact_last_contacted", + "unique": false, + "columnNames": [ + "last_contacted" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_last_contacted` ON `${TABLE_NAME}` (`last_contacted`)" + }, + { + "name": "index_contact_state", + "unique": false, + "columnNames": [ + "state" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_contact_state` ON `${TABLE_NAME}` (`state`)" + } + ], + "foreignKeys": [ + { + "table": "account", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "account" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "certificate", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `fingerprint` TEXT NOT NULL, `intermediate` INTEGER NOT NULL, `email` TEXT NOT NULL, `subject` TEXT, `after` INTEGER, `before` INTEGER, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "fingerprint", + "columnName": "fingerprint", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "intermediate", + "columnName": "intermediate", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "email", + "columnName": "email", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "subject", + "columnName": "subject", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "after", + "columnName": "after", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "before", + "columnName": "before", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_certificate_fingerprint_email", + "unique": true, + "columnNames": [ + "fingerprint", + "email" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_certificate_fingerprint_email` ON `${TABLE_NAME}` (`fingerprint`, `email`)" + }, + { + "name": "index_certificate_email", + "unique": false, + "columnNames": [ + "email" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_certificate_email` ON `${TABLE_NAME}` (`email`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "answer", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `favorite` INTEGER NOT NULL, `hide` INTEGER NOT NULL, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "hide", + "columnName": "hide", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "rule", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `folder` INTEGER NOT NULL, `name` TEXT NOT NULL, `order` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `stop` INTEGER NOT NULL, `condition` TEXT NOT NULL, `action` TEXT NOT NULL, `applied` INTEGER NOT NULL, FOREIGN KEY(`folder`) REFERENCES `folder`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "folder", + "columnName": "folder", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "stop", + "columnName": "stop", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "condition", + "columnName": "condition", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "action", + "columnName": "action", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "applied", + "columnName": "applied", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_rule_folder", + "unique": false, + "columnNames": [ + "folder" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rule_folder` ON `${TABLE_NAME}` (`folder`)" + }, + { + "name": "index_rule_order", + "unique": false, + "columnNames": [ + "order" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_rule_order` ON `${TABLE_NAME}` (`order`)" + } + ], + "foreignKeys": [ + { + "table": "folder", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "folder" + ], + "referencedColumns": [ + "id" + ] + } + ] + }, + { + "tableName": "log", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT, `time` INTEGER NOT NULL, `data` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "time", + "columnName": "time", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "data", + "columnName": "data", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_log_time", + "unique": false, + "columnNames": [ + "time" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_log_time` ON `${TABLE_NAME}` (`time`)" + } + ], + "foreignKeys": [] + } + ], + "views": [ + { + "viewName": "account_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT id, pop, name, color, synchronize, notify, auto_seen, created FROM account" + }, + { + "viewName": "identity_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT id, name, email, display, color, synchronize FROM identity" + }, + { + "viewName": "folder_view", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT id, account, name, type, display, color, unified, notify, read_only FROM folder" + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0f6294f5de89616db4a67550990e237c')" + ] + } +} \ No newline at end of file diff --git a/app/src/fdroid/java/eu/faircode/email/ActivityBilling.java b/app/src/fdroid/java/eu/faircode/email/ActivityBilling.java new file mode 100644 index 0000000000..ad8313d9d7 --- /dev/null +++ b/app/src/fdroid/java/eu/faircode/email/ActivityBilling.java @@ -0,0 +1,579 @@ +package eu.faircode.email; + +/* + This file is part of FairEmail. + + FairEmail is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + FairEmail is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with FairEmail. If not, see . + + Copyright 2018-2020 by Marcel Bokhorst (M66B) +*/ + +import android.annotation.SuppressLint; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.Base64; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; +import androidx.lifecycle.Lifecycle; +import androidx.lifecycle.LifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import androidx.lifecycle.OnLifecycleEvent; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; +import androidx.preference.PreferenceManager; +/* +import com.android.billingclient.api.AcknowledgePurchaseParams; +import com.android.billingclient.api.AcknowledgePurchaseResponseListener; +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingClientStateListener; +import com.android.billingclient.api.BillingFlowParams; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.ConsumeParams; +import com.android.billingclient.api.ConsumeResponseListener; +import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.PurchaseHistoryRecord; +import com.android.billingclient.api.PurchaseHistoryResponseListener; +import com.android.billingclient.api.PurchasesUpdatedListener; +import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.SkuDetailsParams; +import com.android.billingclient.api.SkuDetailsResponseListener; +*/ +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.spec.X509EncodedKeySpec; +import java.util.ArrayList; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class ActivityBilling extends ActivityBase implements /*PurchasesUpdatedListener,*/ FragmentManager.OnBackStackChangedListener { + //private BillingClient billingClient = null; + //private Map skuDetails = new HashMap<>(); + private List listeners = new ArrayList<>(); + + static final String ACTION_PURCHASE = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE"; + static final String ACTION_PURCHASE_CHECK = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE_CHECK"; + static final String ACTION_PURCHASE_ERROR = BuildConfig.APPLICATION_ID + ".ACTION_PURCHASE_ERROR"; + + private final static long MAX_SKU_CACHE_DURATION = 24 * 3600 * 1000L; // milliseconds + private final static long MAX_SKU_NOACK_DURATION = 24 * 3600 * 1000L; // milliseconds + + @Override + @SuppressLint("MissingSuperCall") + protected void onCreate(Bundle savedInstanceState) { + onCreate(savedInstanceState, true); + } + + protected void onCreate(Bundle savedInstanceState, boolean standalone) { + super.onCreate(savedInstanceState); + + if (standalone) { + setContentView(R.layout.activity_billing); + + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro"); + fragmentTransaction.commit(); + + getSupportActionBar().setDisplayHomeAsUpEnabled(true); + + getSupportFragmentManager().addOnBackStackChangedListener(this); + } + + if (Helper.isPlayStoreInstall()) { + Log.i("IAB start"); + //billingClient = BillingClient.newBuilder(this) + // .enablePendingPurchases() + // .setListener(this) + // .build(); + //billingClient.startConnection(billingClientStateListener); + } + } + + @Override + public void onBackStackChanged() { + if (getSupportFragmentManager().getBackStackEntryCount() == 0) + finish(); + } + + @Override + protected void onResume() { + super.onResume(); + + LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); + IntentFilter iff = new IntentFilter(); + iff.addAction(ACTION_PURCHASE); + iff.addAction(ACTION_PURCHASE_CHECK); + iff.addAction(ACTION_PURCHASE_ERROR); + lbm.registerReceiver(receiver, iff); + + //if (billingClient != null && billingClient.isReady()) + // queryPurchases(); + } + + @Override + protected void onPause() { + super.onPause(); + + LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(this); + lbm.unregisterReceiver(receiver); + } + + @Override + protected void onDestroy() { + //if (billingClient != null) + // billingClient.endConnection(); + + super.onDestroy(); + } + + @NonNull + static String getSkuPro() { + if (BuildConfig.DEBUG) + return "android.test.purchased"; + else + return BuildConfig.APPLICATION_ID + ".pro"; + } + + private static String getChallenge(Context context) throws NoSuchAlgorithmException { + String android_id = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID); + return Helper.sha256(android_id); + } + + private static String getResponse(Context context) throws NoSuchAlgorithmException { + return Helper.sha256(BuildConfig.APPLICATION_ID + getChallenge(context)); + } + + static boolean activatePro(Context context, Uri data) throws NoSuchAlgorithmException { + String challenge = getChallenge(context); + String response = data.getQueryParameter("response"); + Log.i("IAB challenge=" + challenge); + Log.i("IAB response=" + response); + String expected = getResponse(context); + if (expected.equals(response)) { + Log.i("IAB response valid"); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + prefs.edit() + .putBoolean("pro", true) + .putBoolean("play_store", false) + .apply(); + + WidgetUnified.updateData(context); + return true; + } else { + Log.i("IAB response invalid"); + return false; + } + } + + static boolean isPro(Context context) { + if (BuildConfig.DEBUG && false) + return true; + return PreferenceManager.getDefaultSharedPreferences(context) + .getBoolean("pro", false); + } + + private BroadcastReceiver receiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { + if (ACTION_PURCHASE.equals(intent.getAction())) + onPurchase(intent); + else if (ACTION_PURCHASE_CHECK.equals(intent.getAction())) + ;//onPurchaseCheck(intent); + else if (ACTION_PURCHASE_ERROR.equals(intent.getAction())) + ;//onPurchaseError(intent); + } + } + }; + + private void onPurchase(Intent intent) { + if (Helper.isPlayStoreInstall()) { + //BillingFlowParams.Builder flowParams = BillingFlowParams.newBuilder(); + //if (skuDetails.containsKey(getSkuPro())) { + // Log.i("IAB purchase SKU=" + skuDetails.get(getSkuPro())); + // flowParams.setSkuDetails(skuDetails.get(getSkuPro())); + //} + + //BillingResult result = billingClient.launchBillingFlow(this, flowParams.build()); + //if (result.getResponseCode() != BillingClient.BillingResponseCode.OK) + // reportError(result, "IAB launch billing flow"); + } else + try { + Uri uri = Uri.parse(BuildConfig.PRO_FEATURES_URI + "?challenge=" + getChallenge(this)); + Helper.view(this, uri, true); + } catch (NoSuchAlgorithmException ex) { + Log.unexpectedError(getSupportFragmentManager(), ex); + } + } +/* + private void onPurchaseCheck(Intent intent) { + billingClient.queryPurchaseHistoryAsync(BillingClient.SkuType.INAPP, new PurchaseHistoryResponseListener() { + @Override + public void onPurchaseHistoryResponse(BillingResult result, List records) { + if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) { + for (PurchaseHistoryRecord record : records) + Log.i("IAB history=" + record.toString()); + + queryPurchases(); + + ToastEx.makeText(ActivityBilling.this, R.string.title_setup_done, Toast.LENGTH_LONG).show(); + } else + reportError(result, "IAB history"); + } + }); + } + + private void onPurchaseError(Intent intent) { + String message = intent.getStringExtra("message"); + Uri uri = Uri.parse(Helper.SUPPORT_URI); + if (!TextUtils.isEmpty(message)) + uri = uri.buildUpon().appendQueryParameter("message", "IAB: " + message).build(); + Helper.view(this, uri, true); + } + + private BillingClientStateListener billingClientStateListener = new BillingClientStateListener() { + private int backoff = 4; // seconds + + @Override + public void onBillingSetupFinished(BillingResult result) { + if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) { + for (IBillingListener listener : listeners) + listener.onConnected(); + + backoff = 4; + queryPurchases(); + } else + reportError(result, "IAB connected"); + } + + @Override + public void onBillingServiceDisconnected() { + for (IBillingListener listener : listeners) + listener.onDisconnected(); + + backoff *= 2; + retry(backoff); + } + }; + + private void retry(int backoff) { + Log.i("IAB connect retry in " + backoff + " s"); + + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + if (!billingClient.isReady()) + billingClient.startConnection(billingClientStateListener); + } + }, backoff * 1000L); + } + + @Override + public void onPurchasesUpdated(BillingResult result, @Nullable List purchases) { + if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) + checkPurchases(purchases); + else + reportError(result, "IAB purchases updated"); + } + + private void queryPurchases() { + Purchase.PurchasesResult result = billingClient.queryPurchases(BillingClient.SkuType.INAPP); + if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) + checkPurchases(result.getPurchasesList()); + else + reportError(result.getBillingResult(), "IAB query purchases"); + } +*/ + interface IBillingListener { + void onConnected(); + + void onDisconnected(); + + void onSkuDetails(String sku, String price); + + void onPurchasePending(String sku); + + void onPurchased(String sku); + + void onError(String message); + } + + void addBillingListener(final IBillingListener listener, LifecycleOwner owner) { + Log.i("IAB adding billing listener=" + listener); + listeners.add(listener); + + //if (billingClient != null) + // if (billingClient.isReady()) { + // listener.onConnected(); + // queryPurchases(); + // } else + // listener.onDisconnected(); + + owner.getLifecycle().addObserver(new LifecycleObserver() { + @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) + public void onDestroyed() { + Log.i("IAB removing billing listener=" + listener); + listeners.remove(listener); + } + }); + } +/* + private void checkPurchases(List purchases) { + Log.i("IAB purchases=" + (purchases == null ? null : purchases.size())); + + List query = new ArrayList<>(); + query.add(getSkuPro()); + + if (purchases != null) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = prefs.edit(); + if (prefs.getBoolean("play_store", true)) { + long cached = prefs.getLong(getSkuPro() + ".cached", 0); + if (cached + MAX_SKU_CACHE_DURATION < new Date().getTime()) { + Log.i("IAB cache expired=" + new Date(cached)); + editor.remove("pro"); + } else + Log.i("IAB caching until=" + new Date(cached + MAX_SKU_CACHE_DURATION)); + } + + for (Purchase purchase : purchases) + try { + query.remove(purchase.getSku()); + + long time = purchase.getPurchaseTime(); + Log.i("IAB SKU=" + purchase.getSku() + + " purchased=" + isPurchased(purchase) + + " valid=" + isPurchaseValid(purchase) + + " time=" + new Date(time)); + + //if (new Date().getTime() - purchase.getPurchaseTime() > 3 * 60 * 1000L) { + // consumePurchase(purchase); + // continue; + //} + + for (IBillingListener listener : listeners) + if (isPurchaseValid(purchase)) + listener.onPurchased(purchase.getSku()); + else + listener.onPurchasePending(purchase.getSku()); + + if (isPurchased(purchase)) { + byte[] decodedKey = Base64.decode(getString(R.string.public_key), Base64.DEFAULT); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PublicKey publicKey = keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); + Signature sig = Signature.getInstance("SHA1withRSA"); + sig.initVerify(publicKey); + sig.update(purchase.getOriginalJson().getBytes()); + if (sig.verify(Base64.decode(purchase.getSignature(), Base64.DEFAULT))) { + Log.i("IAB valid signature"); + if (getSkuPro().equals(purchase.getSku())) { + if (isPurchaseValid(purchase)) { + editor.putBoolean("pro", true); + editor.putLong(purchase.getSku() + ".cached", new Date().getTime()); + } + + if (!purchase.isAcknowledged()) + acknowledgePurchase(purchase, 0); + } + } else { + Log.w("IAB invalid signature"); + editor.putBoolean("pro", false); + reportError(null, "Invalid purchase"); + } + } + } catch (Throwable ex) { + reportError(null, Log.formatThrowable(ex, false)); + } + + editor.apply(); + + WidgetUnified.updateData(this); + } + + if (query.size() > 0) + querySkus(query); + } + + private void querySkus(List query) { + Log.i("IAB query SKUs"); + SkuDetailsParams.Builder builder = SkuDetailsParams.newBuilder(); + builder.setSkusList(query); + builder.setType(BillingClient.SkuType.INAPP); + billingClient.querySkuDetailsAsync(builder.build(), + new SkuDetailsResponseListener() { + @Override + public void onSkuDetailsResponse(BillingResult result, List skuDetailsList) { + if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) { + for (SkuDetails skuDetail : skuDetailsList) { + Log.i("IAB SKU detail=" + skuDetail); + skuDetails.put(skuDetail.getSku(), skuDetail); + for (IBillingListener listener : listeners) + listener.onSkuDetails(skuDetail.getSku(), skuDetail.getPrice()); + } + } else + reportError(result, "IAB query SKUs"); + } + }); + } + + private void consumePurchase(final Purchase purchase) { + Log.i("IAB SKU=" + purchase.getSku() + " consuming"); + ConsumeParams params = ConsumeParams.newBuilder() + .setPurchaseToken(purchase.getPurchaseToken()) + .build(); + billingClient.consumeAsync(params, new ConsumeResponseListener() { + @Override + public void onConsumeResponse(BillingResult result, String purchaseToken) { + if (result.getResponseCode() != BillingClient.BillingResponseCode.OK) + reportError(result, "IAB consumed SKU=" + purchase.getSku()); + } + }); + } + + private void acknowledgePurchase(final Purchase purchase, int retry) { + Log.i("IAB acknowledging purchase SKU=" + purchase.getSku()); + AcknowledgePurchaseParams params = + AcknowledgePurchaseParams.newBuilder() + .setPurchaseToken(purchase.getPurchaseToken()) + .build(); + billingClient.acknowledgePurchase(params, new AcknowledgePurchaseResponseListener() { + @Override + public void onAcknowledgePurchaseResponse(BillingResult result) { + if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) { + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(ActivityBilling.this); + SharedPreferences.Editor editor = prefs.edit(); + editor.putBoolean("pro", true); + editor.putLong(purchase.getSku() + ".cached", new Date().getTime()); + editor.apply(); + + for (IBillingListener listener : listeners) + listener.onPurchased(purchase.getSku()); + + WidgetUnified.updateData(ActivityBilling.this); + } else { + if (retry < 3) { + new Handler().postDelayed(new Runnable() { + @Override + public void run() { + acknowledgePurchase(purchase, retry + 1); + } + }, (retry + 1) * 10 * 1000L); + } else + reportError(result, "IAB acknowledged SKU=" + purchase.getSku()); + } + } + }); + } + + private boolean isPurchased(Purchase purchase) { + return (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED); + } + + private boolean isPurchaseValid(Purchase purchase) { + return (isPurchased(purchase) && + (purchase.isAcknowledged() || + purchase.getPurchaseTime() + MAX_SKU_NOACK_DURATION > new Date().getTime())); + } + + private void reportError(BillingResult result, String stage) { + String message; + if (result == null) + message = stage; + else { + String debug = result.getDebugMessage(); + message = getBillingResponseText(result) + (debug == null ? "" : " " + debug) + " " + stage; + + // https://developer.android.com/reference/com/android/billingclient/api/BillingClient.BillingResponse#service_disconnected + if (result.getResponseCode() == BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) + retry(60); + } + + EntityLog.log(this, message); + + if (result.getResponseCode() != BillingClient.BillingResponseCode.USER_CANCELED) + for (IBillingListener listener : listeners) + listener.onError(message); + } + + private static String getBillingResponseText(BillingResult result) { + switch (result.getResponseCode()) { + case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE: + // Billing API version is not supported for the type requested + return "BILLING_UNAVAILABLE"; + + case BillingClient.BillingResponseCode.DEVELOPER_ERROR: + // Invalid arguments provided to the API. + return "DEVELOPER_ERROR"; + + case BillingClient.BillingResponseCode.ERROR: + // Fatal error during the API action + return "ERROR"; + + case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED: + // Requested feature is not supported by Play Store on the current device. + return "FEATURE_NOT_SUPPORTED"; + + case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED: + // Failure to purchase since item is already owned + return "ITEM_ALREADY_OWNED"; + + case BillingClient.BillingResponseCode.ITEM_NOT_OWNED: + // Failure to consume since item is not owned + return "ITEM_NOT_OWNED"; + + case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE: + // Requested product is not available for purchase + return "ITEM_UNAVAILABLE"; + + case BillingClient.BillingResponseCode.OK: + // Success + return "OK"; + + case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED: + // Play Store service is not connected now - potentially transient state. + return "SERVICE_DISCONNECTED"; + + case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE: + // Network connection is down + return "SERVICE_UNAVAILABLE"; + + case BillingClient.BillingResponseCode.SERVICE_TIMEOUT: + // The request has reached the maximum timeout before Google Play responds. + return "SERVICE_TIMEOUT"; + + case BillingClient.BillingResponseCode.USER_CANCELED: + // User pressed back or canceled a dialog + return "USER_CANCELED"; + + default: + return Integer.toString(result.getResponseCode()); + } + } + */ +} diff --git a/app/src/main/java/eu/faircode/email/ActivityBilling.java b/app/src/iab/java/eu/faircode/email/ActivityBilling.java similarity index 100% rename from app/src/main/java/eu/faircode/email/ActivityBilling.java rename to app/src/iab/java/eu/faircode/email/ActivityBilling.java diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 15fbe68321..943bbf2e80 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,11 +38,42 @@ android:required="false" /> - + + + + + + + + + + + + + + - + + + - + + + + + + + + + + + + extends PositionalDataSource { mDb.setTransactionSuccessful(); list = rows; } + } catch (Throwable ex) { + eu.faircode.email.Log.w(ex); } finally { if (cursor != null) { cursor.close(); diff --git a/app/src/main/java/com/sun/activation/registries/LogSupport.java b/app/src/main/java/com/sun/activation/registries/LogSupport.java new file mode 100644 index 0000000000..3228f3c790 --- /dev/null +++ b/app/src/main/java/com/sun/activation/registries/LogSupport.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +import java.io.*; +import java.util.logging.*; + +/** + * Logging related methods. + */ +public class LogSupport { + private static boolean debug = false; + private static Logger logger; + private static final Level level = Level.FINE; + + static { + try { + debug = Boolean.getBoolean("javax.activation.debug"); + } catch (Throwable t) { + // ignore any errors + } + logger = Logger.getLogger("javax.activation"); + } + + /** + * Constructor. + */ + private LogSupport() { + // private constructor, can't create instances + } + + public static void log(String msg) { + if (debug) + System.out.println(msg); + logger.log(level, msg); + } + + public static void log(String msg, Throwable t) { + if (debug) + System.out.println(msg + "; Exception: " + t); + logger.log(level, msg, t); + } + + public static boolean isLoggable() { + return debug || logger.isLoggable(level); + } +} diff --git a/app/src/main/java/com/sun/activation/registries/MailcapFile.java b/app/src/main/java/com/sun/activation/registries/MailcapFile.java new file mode 100644 index 0000000000..afcb5d6ef2 --- /dev/null +++ b/app/src/main/java/com/sun/activation/registries/MailcapFile.java @@ -0,0 +1,548 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +import java.io.*; +import java.util.*; + +public class MailcapFile { + + /** + * A Map indexed by MIME type (string) that references + * a Map of commands for each type. The comand Map + * is indexed by the command name and references a List of + * class names (strings) for each command. + */ + private Map type_hash = new HashMap(); + + /** + * Another Map like above, but for fallback entries. + */ + private Map fallback_hash = new HashMap(); + + /** + * A Map indexed by MIME type (string) that references + * a List of native commands (string) corresponding to the type. + */ + private Map native_commands = new HashMap(); + + private static boolean addReverse = false; + + static { + try { + addReverse = Boolean.getBoolean("javax.activation.addreverse"); + } catch (Throwable t) { + // ignore any errors + } + } + + /** + * The constructor that takes a filename as an argument. + * + * @param new_fname The file name of the mailcap file. + */ + public MailcapFile(String new_fname) throws IOException { + if (LogSupport.isLoggable()) + LogSupport.log("new MailcapFile: file " + new_fname); + FileReader reader = null; + try { + reader = new FileReader(new_fname); + parse(new BufferedReader(reader)); + } finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException ex) { } + } + } + } + + /** + * The constructor that takes an input stream as an argument. + * + * @param is the input stream + */ + public MailcapFile(InputStream is) throws IOException { + if (LogSupport.isLoggable()) + LogSupport.log("new MailcapFile: InputStream"); + parse(new BufferedReader(new InputStreamReader(is, "iso-8859-1"))); + } + + /** + * Mailcap file default constructor. + */ + public MailcapFile() { + if (LogSupport.isLoggable()) + LogSupport.log("new MailcapFile: default"); + } + + /** + * Get the Map of MailcapEntries based on the MIME type. + * + *

+ * Semantics: First check for the literal mime type, + * if that fails looks for wildcard type/* and return that. Return the + * list of all that hit. + */ + public Map getMailcapList(String mime_type) { + Map search_result = null; + Map wildcard_result = null; + + // first try the literal + search_result = (Map)type_hash.get(mime_type); + + // ok, now try the wildcard + int separator = mime_type.indexOf('/'); + String subtype = mime_type.substring(separator + 1); + if (!subtype.equals("*")) { + String type = mime_type.substring(0, separator + 1) + "*"; + wildcard_result = (Map)type_hash.get(type); + + if (wildcard_result != null) { // damn, we have to merge!!! + if (search_result != null) + search_result = + mergeResults(search_result, wildcard_result); + else + search_result = wildcard_result; + } + } + return search_result; + } + + /** + * Get the Map of fallback MailcapEntries based on the MIME type. + * + *

+ * Semantics: First check for the literal mime type, + * if that fails looks for wildcard type/* and return that. Return the + * list of all that hit. + */ + public Map getMailcapFallbackList(String mime_type) { + Map search_result = null; + Map wildcard_result = null; + + // first try the literal + search_result = (Map)fallback_hash.get(mime_type); + + // ok, now try the wildcard + int separator = mime_type.indexOf('/'); + String subtype = mime_type.substring(separator + 1); + if (!subtype.equals("*")) { + String type = mime_type.substring(0, separator + 1) + "*"; + wildcard_result = (Map)fallback_hash.get(type); + + if (wildcard_result != null) { // damn, we have to merge!!! + if (search_result != null) + search_result = + mergeResults(search_result, wildcard_result); + else + search_result = wildcard_result; + } + } + return search_result; + } + + /** + * Return all the MIME types known to this mailcap file. + */ + public String[] getMimeTypes() { + Set types = new HashSet(type_hash.keySet()); + types.addAll(fallback_hash.keySet()); + types.addAll(native_commands.keySet()); + String[] mts = new String[types.size()]; + mts = (String[])types.toArray(mts); + return mts; + } + + /** + * Return all the native comands for the given MIME type. + */ + public String[] getNativeCommands(String mime_type) { + String[] cmds = null; + List v = + (List)native_commands.get(mime_type.toLowerCase(Locale.ENGLISH)); + if (v != null) { + cmds = new String[v.size()]; + cmds = (String[])v.toArray(cmds); + } + return cmds; + } + + /** + * Merge the first hash into the second. + * This merge will only effect the hashtable that is + * returned, we don't want to touch the one passed in since + * its integrity must be maintained. + */ + private Map mergeResults(Map first, Map second) { + Iterator verb_enum = second.keySet().iterator(); + Map clonedHash = new HashMap(first); + + // iterate through the verbs in the second map + while (verb_enum.hasNext()) { + String verb = (String)verb_enum.next(); + List cmdVector = (List)clonedHash.get(verb); + if (cmdVector == null) { + clonedHash.put(verb, second.get(verb)); + } else { + // merge the two + List oldV = (List)second.get(verb); + cmdVector = new ArrayList(cmdVector); + cmdVector.addAll(oldV); + clonedHash.put(verb, cmdVector); + } + } + return clonedHash; + } + + /** + * appendToMailcap: Append to this Mailcap DB, use the mailcap + * format: + * Comment == "# comment string + * Entry == "mimetype; javabeanclass
+ * + * Example: + * # this is a comment + * image/gif jaf.viewers.ImageViewer + */ + public void appendToMailcap(String mail_cap) { + if (LogSupport.isLoggable()) + LogSupport.log("appendToMailcap: " + mail_cap); + try { + parse(new StringReader(mail_cap)); + } catch (IOException ex) { + // can't happen + } + } + + /** + * parse file into a hash table of MC Type Entry Obj + */ + private void parse(Reader reader) throws IOException { + BufferedReader buf_reader = new BufferedReader(reader); + String line = null; + String continued = null; + + while ((line = buf_reader.readLine()) != null) { + // LogSupport.log("parsing line: " + line); + + line = line.trim(); + + try { + if (line.charAt(0) == '#') + continue; + if (line.charAt(line.length() - 1) == '\\') { + if (continued != null) + continued += line.substring(0, line.length() - 1); + else + continued = line.substring(0, line.length() - 1); + } else if (continued != null) { + // handle the two strings + continued = continued + line; + // LogSupport.log("parse: " + continued); + try { + parseLine(continued); + } catch (MailcapParseException e) { + //e.printStackTrace(); + } + continued = null; + } + else { + // LogSupport.log("parse: " + line); + try { + parseLine(line); + // LogSupport.log("hash.size = " + type_hash.size()); + } catch (MailcapParseException e) { + //e.printStackTrace(); + } + } + } catch (StringIndexOutOfBoundsException e) {} + } + } + + /** + * A routine to parse individual entries in a Mailcap file. + * + * Note that this routine does not handle line continuations. + * They should have been handled prior to calling this routine. + */ + protected void parseLine(String mailcapEntry) + throws MailcapParseException, IOException { + MailcapTokenizer tokenizer = new MailcapTokenizer(mailcapEntry); + tokenizer.setIsAutoquoting(false); + + if (LogSupport.isLoggable()) + LogSupport.log("parse: " + mailcapEntry); + // parse the primary type + int currentToken = tokenizer.nextToken(); + if (currentToken != MailcapTokenizer.STRING_TOKEN) { + reportParseError(MailcapTokenizer.STRING_TOKEN, currentToken, + tokenizer.getCurrentTokenValue()); + } + String primaryType = + tokenizer.getCurrentTokenValue().toLowerCase(Locale.ENGLISH); + String subType = "*"; + + // parse the '/' between primary and sub + // if it's not present that's ok, we just don't have a subtype + currentToken = tokenizer.nextToken(); + if ((currentToken != MailcapTokenizer.SLASH_TOKEN) && + (currentToken != MailcapTokenizer.SEMICOLON_TOKEN)) { + reportParseError(MailcapTokenizer.SLASH_TOKEN, + MailcapTokenizer.SEMICOLON_TOKEN, currentToken, + tokenizer.getCurrentTokenValue()); + } + + // only need to look for a sub type if we got a '/' + if (currentToken == MailcapTokenizer.SLASH_TOKEN) { + // parse the sub type + currentToken = tokenizer.nextToken(); + if (currentToken != MailcapTokenizer.STRING_TOKEN) { + reportParseError(MailcapTokenizer.STRING_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + subType = + tokenizer.getCurrentTokenValue().toLowerCase(Locale.ENGLISH); + + // get the next token to simplify the next step + currentToken = tokenizer.nextToken(); + } + + String mimeType = primaryType + "/" + subType; + + if (LogSupport.isLoggable()) + LogSupport.log(" Type: " + mimeType); + + // now setup the commands hashtable + Map commands = new LinkedHashMap(); // keep commands in order found + + // parse the ';' that separates the type from the parameters + if (currentToken != MailcapTokenizer.SEMICOLON_TOKEN) { + reportParseError(MailcapTokenizer.SEMICOLON_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + // eat it + + // parse the required view command + tokenizer.setIsAutoquoting(true); + currentToken = tokenizer.nextToken(); + tokenizer.setIsAutoquoting(false); + if ((currentToken != MailcapTokenizer.STRING_TOKEN) && + (currentToken != MailcapTokenizer.SEMICOLON_TOKEN)) { + reportParseError(MailcapTokenizer.STRING_TOKEN, + MailcapTokenizer.SEMICOLON_TOKEN, currentToken, + tokenizer.getCurrentTokenValue()); + } + + if (currentToken == MailcapTokenizer.STRING_TOKEN) { + // have a native comand, save the entire mailcap entry + //String nativeCommand = tokenizer.getCurrentTokenValue(); + List v = (List)native_commands.get(mimeType); + if (v == null) { + v = new ArrayList(); + v.add(mailcapEntry); + native_commands.put(mimeType, v); + } else { + // XXX - check for duplicates? + v.add(mailcapEntry); + } + } + + // only have to get the next token if the current one isn't a ';' + if (currentToken != MailcapTokenizer.SEMICOLON_TOKEN) { + currentToken = tokenizer.nextToken(); + } + + // look for a ';' which will indicate whether + // a parameter list is present or not + if (currentToken == MailcapTokenizer.SEMICOLON_TOKEN) { + boolean isFallback = false; + do { + // eat the ';' + + // parse the parameter name + currentToken = tokenizer.nextToken(); + if (currentToken != MailcapTokenizer.STRING_TOKEN) { + reportParseError(MailcapTokenizer.STRING_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + String paramName = tokenizer.getCurrentTokenValue(). + toLowerCase(Locale.ENGLISH); + + // parse the '=' which separates the name from the value + currentToken = tokenizer.nextToken(); + if ((currentToken != MailcapTokenizer.EQUALS_TOKEN) && + (currentToken != MailcapTokenizer.SEMICOLON_TOKEN) && + (currentToken != MailcapTokenizer.EOI_TOKEN)) { + reportParseError(MailcapTokenizer.EQUALS_TOKEN, + MailcapTokenizer.SEMICOLON_TOKEN, + MailcapTokenizer.EOI_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + + // we only have a useful command if it is named + if (currentToken == MailcapTokenizer.EQUALS_TOKEN) { + // eat it + + // parse the parameter value (which is autoquoted) + tokenizer.setIsAutoquoting(true); + currentToken = tokenizer.nextToken(); + tokenizer.setIsAutoquoting(false); + if (currentToken != MailcapTokenizer.STRING_TOKEN) { + reportParseError(MailcapTokenizer.STRING_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + String paramValue = + tokenizer.getCurrentTokenValue(); + + // add the class to the list iff it is one we care about + if (paramName.startsWith("x-java-")) { + String commandName = paramName.substring(7); + // 7 == "x-java-".length + + if (commandName.equals("fallback-entry") && + paramValue.equalsIgnoreCase("true")) { + isFallback = true; + } else { + + // setup the class entry list + if (LogSupport.isLoggable()) + LogSupport.log(" Command: " + commandName + + ", Class: " + paramValue); + List classes = (List)commands.get(commandName); + if (classes == null) { + classes = new ArrayList(); + commands.put(commandName, classes); + } + if (addReverse) + classes.add(0, paramValue); + else + classes.add(paramValue); + } + } + + // set up the next iteration + currentToken = tokenizer.nextToken(); + } + } while (currentToken == MailcapTokenizer.SEMICOLON_TOKEN); + + Map masterHash = isFallback ? fallback_hash : type_hash; + Map curcommands = + (Map)masterHash.get(mimeType); + if (curcommands == null) { + masterHash.put(mimeType, commands); + } else { + if (LogSupport.isLoggable()) + LogSupport.log("Merging commands for type " + mimeType); + // have to merge current and new commands + // first, merge list of classes for commands already known + Iterator cn = curcommands.keySet().iterator(); + while (cn.hasNext()) { + String cmdName = (String)cn.next(); + List ccv = (List)curcommands.get(cmdName); + List cv = (List)commands.get(cmdName); + if (cv == null) + continue; + // add everything in cv to ccv, if it's not already there + Iterator cvn = cv.iterator(); + while (cvn.hasNext()) { + String clazz = (String)cvn.next(); + if (!ccv.contains(clazz)) + if (addReverse) + ccv.add(0, clazz); + else + ccv.add(clazz); + } + } + // now, add commands not previously known + cn = commands.keySet().iterator(); + while (cn.hasNext()) { + String cmdName = (String)cn.next(); + if (curcommands.containsKey(cmdName)) + continue; + List cv = (List)commands.get(cmdName); + curcommands.put(cmdName, cv); + } + } + } else if (currentToken != MailcapTokenizer.EOI_TOKEN) { + reportParseError(MailcapTokenizer.EOI_TOKEN, + MailcapTokenizer.SEMICOLON_TOKEN, + currentToken, tokenizer.getCurrentTokenValue()); + } + } + + protected static void reportParseError(int expectedToken, int actualToken, + String actualTokenValue) throws MailcapParseException { + throw new MailcapParseException("Encountered a " + + MailcapTokenizer.nameForToken(actualToken) + " token (" + + actualTokenValue + ") while expecting a " + + MailcapTokenizer.nameForToken(expectedToken) + " token."); + } + + protected static void reportParseError(int expectedToken, + int otherExpectedToken, int actualToken, String actualTokenValue) + throws MailcapParseException { + throw new MailcapParseException("Encountered a " + + MailcapTokenizer.nameForToken(actualToken) + " token (" + + actualTokenValue + ") while expecting a " + + MailcapTokenizer.nameForToken(expectedToken) + " or a " + + MailcapTokenizer.nameForToken(otherExpectedToken) + " token."); + } + + protected static void reportParseError(int expectedToken, + int otherExpectedToken, int anotherExpectedToken, int actualToken, + String actualTokenValue) throws MailcapParseException { + if (LogSupport.isLoggable()) + LogSupport.log("PARSE ERROR: " + "Encountered a " + + MailcapTokenizer.nameForToken(actualToken) + " token (" + + actualTokenValue + ") while expecting a " + + MailcapTokenizer.nameForToken(expectedToken) + ", a " + + MailcapTokenizer.nameForToken(otherExpectedToken) + ", or a " + + MailcapTokenizer.nameForToken(anotherExpectedToken) + " token."); + throw new MailcapParseException("Encountered a " + + MailcapTokenizer.nameForToken(actualToken) + " token (" + + actualTokenValue + ") while expecting a " + + MailcapTokenizer.nameForToken(expectedToken) + ", a " + + MailcapTokenizer.nameForToken(otherExpectedToken) + ", or a " + + MailcapTokenizer.nameForToken(anotherExpectedToken) + " token."); + } + + /** for debugging + public static void main(String[] args) throws Exception { + Map masterHash = new HashMap(); + for (int i = 0; i < args.length; ++i) { + System.out.println("Entry " + i + ": " + args[i]); + parseLine(args[i], masterHash); + } + + Enumeration types = masterHash.keys(); + while (types.hasMoreElements()) { + String key = (String)types.nextElement(); + System.out.println("MIME Type: " + key); + + Map commandHash = (Map)masterHash.get(key); + Enumeration commands = commandHash.keys(); + while (commands.hasMoreElements()) { + String command = (String)commands.nextElement(); + System.out.println(" Command: " + command); + + Vector classes = (Vector)commandHash.get(command); + for (int i = 0; i < classes.size(); ++i) { + System.out.println(" Class: " + + (String)classes.elementAt(i)); + } + } + + System.out.println(""); + } + } + */ +} diff --git a/app/src/main/java/com/sun/activation/registries/MailcapParseException.java b/app/src/main/java/com/sun/activation/registries/MailcapParseException.java new file mode 100644 index 0000000000..754c405e5d --- /dev/null +++ b/app/src/main/java/com/sun/activation/registries/MailcapParseException.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +/** + * A class to encapsulate Mailcap parsing related exceptions + */ +public class MailcapParseException extends Exception { + + public MailcapParseException() { + super(); + } + + public MailcapParseException(String inInfo) { + super(inInfo); + } +} diff --git a/app/src/main/java/com/sun/activation/registries/MailcapTokenizer.java b/app/src/main/java/com/sun/activation/registries/MailcapTokenizer.java new file mode 100644 index 0000000000..6437930e54 --- /dev/null +++ b/app/src/main/java/com/sun/activation/registries/MailcapTokenizer.java @@ -0,0 +1,307 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +/** + * A tokenizer for strings in the form of "foo/bar; prop1=val1; ... ". + * Useful for parsing MIME content types. + */ +public class MailcapTokenizer { + + public static final int UNKNOWN_TOKEN = 0; + public static final int START_TOKEN = 1; + public static final int STRING_TOKEN = 2; + public static final int EOI_TOKEN = 5; + public static final int SLASH_TOKEN = '/'; + public static final int SEMICOLON_TOKEN = ';'; + public static final int EQUALS_TOKEN = '='; + + /** + * Constructor + * + * @param inputString the string to tokenize + */ + public MailcapTokenizer(String inputString) { + data = inputString; + dataIndex = 0; + dataLength = inputString.length(); + + currentToken = START_TOKEN; + currentTokenValue = ""; + + isAutoquoting = false; + autoquoteChar = ';'; + } + + /** + * Set whether auto-quoting is on or off. + * + * Auto-quoting means that all characters after the first + * non-whitespace, non-control character up to the auto-quote + * terminator character or EOI (minus any whitespace immediatley + * preceeding it) is considered a token. + * + * This is required for handling command strings in a mailcap entry. + */ + public void setIsAutoquoting(boolean value) { + isAutoquoting = value; + } + + /** + * Retrieve current token. + * + * @return The current token value + */ + public int getCurrentToken() { + return currentToken; + } + + /* + * Get a String that describes the given token. + */ + public static String nameForToken(int token) { + String name = "really unknown"; + + switch(token) { + case UNKNOWN_TOKEN: + name = "unknown"; + break; + case START_TOKEN: + name = "start"; + break; + case STRING_TOKEN: + name = "string"; + break; + case EOI_TOKEN: + name = "EOI"; + break; + case SLASH_TOKEN: + name = "'/'"; + break; + case SEMICOLON_TOKEN: + name = "';'"; + break; + case EQUALS_TOKEN: + name = "'='"; + break; + } + + return name; + } + + /* + * Retrieve current token value. + * + * @return A String containing the current token value + */ + public String getCurrentTokenValue() { + return currentTokenValue; + } + + /* + * Process the next token. + * + * @return the next token + */ + public int nextToken() { + if (dataIndex < dataLength) { + // skip white space + while ((dataIndex < dataLength) && + (isWhiteSpaceChar(data.charAt(dataIndex)))) { + ++dataIndex; + } + + if (dataIndex < dataLength) { + // examine the current character and see what kind of token we have + char c = data.charAt(dataIndex); + if (isAutoquoting) { + if (c == ';' || c == '=') { + currentToken = c; + currentTokenValue = new Character(c).toString(); + ++dataIndex; + } else { + processAutoquoteToken(); + } + } else { + if (isStringTokenChar(c)) { + processStringToken(); + } else if ((c == '/') || (c == ';') || (c == '=')) { + currentToken = c; + currentTokenValue = new Character(c).toString(); + ++dataIndex; + } else { + currentToken = UNKNOWN_TOKEN; + currentTokenValue = new Character(c).toString(); + ++dataIndex; + } + } + } else { + currentToken = EOI_TOKEN; + currentTokenValue = null; + } + } else { + currentToken = EOI_TOKEN; + currentTokenValue = null; + } + + return currentToken; + } + + private void processStringToken() { + // capture the initial index + int initialIndex = dataIndex; + + // skip to 1st non string token character + while ((dataIndex < dataLength) && + isStringTokenChar(data.charAt(dataIndex))) { + ++dataIndex; + } + + currentToken = STRING_TOKEN; + currentTokenValue = data.substring(initialIndex, dataIndex); + } + + private void processAutoquoteToken() { + // capture the initial index + int initialIndex = dataIndex; + + // now skip to the 1st non-escaped autoquote termination character + // XXX - doesn't actually consider escaping + boolean foundTerminator = false; + while ((dataIndex < dataLength) && !foundTerminator) { + char c = data.charAt(dataIndex); + if (c != autoquoteChar) { + ++dataIndex; + } else { + foundTerminator = true; + } + } + + currentToken = STRING_TOKEN; + currentTokenValue = + fixEscapeSequences(data.substring(initialIndex, dataIndex)); + } + + private static boolean isSpecialChar(char c) { + boolean lAnswer = false; + + switch(c) { + case '(': + case ')': + case '<': + case '>': + case '@': + case ',': + case ';': + case ':': + case '\\': + case '"': + case '/': + case '[': + case ']': + case '?': + case '=': + lAnswer = true; + break; + } + + return lAnswer; + } + + private static boolean isControlChar(char c) { + return Character.isISOControl(c); + } + + private static boolean isWhiteSpaceChar(char c) { + return Character.isWhitespace(c); + } + + private static boolean isStringTokenChar(char c) { + return !isSpecialChar(c) && !isControlChar(c) && !isWhiteSpaceChar(c); + } + + private static String fixEscapeSequences(String inputString) { + int inputLength = inputString.length(); + StringBuffer buffer = new StringBuffer(); + buffer.ensureCapacity(inputLength); + + for (int i = 0; i < inputLength; ++i) { + char currentChar = inputString.charAt(i); + if (currentChar != '\\') { + buffer.append(currentChar); + } else { + if (i < inputLength - 1) { + char nextChar = inputString.charAt(i + 1); + buffer.append(nextChar); + + // force a skip over the next character too + ++i; + } else { + buffer.append(currentChar); + } + } + } + + return buffer.toString(); + } + + private String data; + private int dataIndex; + private int dataLength; + private int currentToken; + private String currentTokenValue; + private boolean isAutoquoting; + private char autoquoteChar; + + /* + public static void main(String[] args) { + for (int i = 0; i < args.length; ++i) { + MailcapTokenizer tokenizer = new MailcapTokenizer(args[i]); + + System.out.println("Original: |" + args[i] + "|"); + + int currentToken = tokenizer.nextToken(); + while (currentToken != EOI_TOKEN) { + switch(currentToken) { + case UNKNOWN_TOKEN: + System.out.println(" Unknown Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case START_TOKEN: + System.out.println(" Start Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case STRING_TOKEN: + System.out.println(" String Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case EOI_TOKEN: + System.out.println(" EOI Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case SLASH_TOKEN: + System.out.println(" Slash Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case SEMICOLON_TOKEN: + System.out.println(" Semicolon Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + case EQUALS_TOKEN: + System.out.println(" Equals Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + default: + System.out.println(" Really Unknown Token: |" + tokenizer.getCurrentTokenValue() + "|"); + break; + } + + currentToken = tokenizer.nextToken(); + } + + System.out.println(""); + } + } + */ +} diff --git a/app/src/main/java/com/sun/activation/registries/MimeTypeEntry.java b/app/src/main/java/com/sun/activation/registries/MimeTypeEntry.java new file mode 100644 index 0000000000..3582f779dd --- /dev/null +++ b/app/src/main/java/com/sun/activation/registries/MimeTypeEntry.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +import java.lang.*; + +public class MimeTypeEntry { + private String type; + private String extension; + + public MimeTypeEntry(String mime_type, String file_ext) { + type = mime_type; + extension = file_ext; + } + + public String getMIMEType() { + return type; + } + + public String getFileExtension() { + return extension; + } + + public String toString() { + return "MIMETypeEntry: " + type + ", " + extension; + } +} diff --git a/app/src/main/java/com/sun/activation/registries/MimeTypeFile.java b/app/src/main/java/com/sun/activation/registries/MimeTypeFile.java new file mode 100644 index 0000000000..469279125c --- /dev/null +++ b/app/src/main/java/com/sun/activation/registries/MimeTypeFile.java @@ -0,0 +1,302 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Distribution License v. 1.0, which is available at + * http://www.eclipse.org/org/documents/edl-v10.php. + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +package com.sun.activation.registries; + +import java.io.*; +import java.util.*; + +public class MimeTypeFile { + private String fname = null; + private Hashtable type_hash = new Hashtable(); + + /** + * The construtor that takes a filename as an argument. + * + * @param new_fname The file name of the mime types file. + */ + public MimeTypeFile(String new_fname) throws IOException { + File mime_file = null; + FileReader fr = null; + + fname = new_fname; // remember the file name + + mime_file = new File(fname); // get a file object + + fr = new FileReader(mime_file); + + try { + parse(new BufferedReader(fr)); + } finally { + try { + fr.close(); // close it + } catch (IOException e) { + // ignore it + } + } + } + + public MimeTypeFile(InputStream is) throws IOException { + parse(new BufferedReader(new InputStreamReader(is, "iso-8859-1"))); + } + + /** + * Creates an empty DB. + */ + public MimeTypeFile() { + } + + /** + * get the MimeTypeEntry based on the file extension + */ + public MimeTypeEntry getMimeTypeEntry(String file_ext) { + return (MimeTypeEntry)type_hash.get((Object)file_ext); + } + + /** + * Get the MIME type string corresponding to the file extension. + */ + public String getMIMETypeString(String file_ext) { + MimeTypeEntry entry = this.getMimeTypeEntry(file_ext); + + if (entry != null) + return entry.getMIMEType(); + else + return null; + } + + /** + * Appends string of entries to the types registry, must be valid + * .mime.types format. + * A mime.types entry is one of two forms: + * + * type/subtype ext1 ext2 ... + * or + * type=type/subtype desc="description of type" exts=ext1,ext2,... + * + * Example: + * # this is a test + * audio/basic au + * text/plain txt text + * type=application/postscript exts=ps,eps + */ + public void appendToRegistry(String mime_types) { + try { + parse(new BufferedReader(new StringReader(mime_types))); + } catch (IOException ex) { + // can't happen + } + } + + /** + * Parse a stream of mime.types entries. + */ + private void parse(BufferedReader buf_reader) throws IOException { + String line = null, prev = null; + + while ((line = buf_reader.readLine()) != null) { + if (prev == null) + prev = line; + else + prev += line; + int end = prev.length(); + if (prev.length() > 0 && prev.charAt(end - 1) == '\\') { + prev = prev.substring(0, end - 1); + continue; + } + this.parseEntry(prev); + prev = null; + } + if (prev != null) + this.parseEntry(prev); + } + + /** + * Parse single mime.types entry. + */ + private void parseEntry(String line) { + String mime_type = null; + String file_ext = null; + line = line.trim(); + + if (line.length() == 0) // empty line... + return; // BAIL! + + // check to see if this is a comment line? + if (line.charAt(0) == '#') + return; // then we are done! + + // is it a new format line or old format? + if (line.indexOf('=') > 0) { + // new format + LineTokenizer lt = new LineTokenizer(line); + while (lt.hasMoreTokens()) { + String name = lt.nextToken(); + String value = null; + if (lt.hasMoreTokens() && lt.nextToken().equals("=") && + lt.hasMoreTokens()) + value = lt.nextToken(); + if (value == null) { + if (LogSupport.isLoggable()) + LogSupport.log("Bad .mime.types entry: " + line); + return; + } + if (name.equals("type")) + mime_type = value; + else if (name.equals("exts")) { + StringTokenizer st = new StringTokenizer(value, ","); + while (st.hasMoreTokens()) { + file_ext = st.nextToken(); + MimeTypeEntry entry = + new MimeTypeEntry(mime_type, file_ext); + type_hash.put(file_ext, entry); + if (LogSupport.isLoggable()) + LogSupport.log("Added: " + entry.toString()); + } + } + } + } else { + // old format + // count the tokens + StringTokenizer strtok = new StringTokenizer(line); + int num_tok = strtok.countTokens(); + + if (num_tok == 0) // empty line + return; + + mime_type = strtok.nextToken(); // get the MIME type + + while (strtok.hasMoreTokens()) { + MimeTypeEntry entry = null; + + file_ext = strtok.nextToken(); + entry = new MimeTypeEntry(mime_type, file_ext); + type_hash.put(file_ext, entry); + if (LogSupport.isLoggable()) + LogSupport.log("Added: " + entry.toString()); + } + } + } + + // for debugging + /* + public static void main(String[] argv) throws Exception { + MimeTypeFile mf = new MimeTypeFile(argv[0]); + System.out.println("ext " + argv[1] + " type " + + mf.getMIMETypeString(argv[1])); + System.exit(0); + } + */ +} + +class LineTokenizer { + private int currentPosition; + private int maxPosition; + private String str; + private Vector stack = new Vector(); + private static final String singles = "="; // single character tokens + + /** + * Constructs a tokenizer for the specified string. + *

+ * + * @param str a string to be parsed. + */ + public LineTokenizer(String str) { + currentPosition = 0; + this.str = str; + maxPosition = str.length(); + } + + /** + * Skips white space. + */ + private void skipWhiteSpace() { + while ((currentPosition < maxPosition) && + Character.isWhitespace(str.charAt(currentPosition))) { + currentPosition++; + } + } + + /** + * Tests if there are more tokens available from this tokenizer's string. + * + * @return true if there are more tokens available from this + * tokenizer's string; false otherwise. + */ + public boolean hasMoreTokens() { + if (stack.size() > 0) + return true; + skipWhiteSpace(); + return (currentPosition < maxPosition); + } + + /** + * Returns the next token from this tokenizer. + * + * @return the next token from this tokenizer. + * @exception NoSuchElementException if there are no more tokens in this + * tokenizer's string. + */ + public String nextToken() { + int size = stack.size(); + if (size > 0) { + String t = (String)stack.elementAt(size - 1); + stack.removeElementAt(size - 1); + return t; + } + skipWhiteSpace(); + + if (currentPosition >= maxPosition) { + throw new NoSuchElementException(); + } + + int start = currentPosition; + char c = str.charAt(start); + if (c == '"') { + currentPosition++; + boolean filter = false; + while (currentPosition < maxPosition) { + c = str.charAt(currentPosition++); + if (c == '\\') { + currentPosition++; + filter = true; + } else if (c == '"') { + String s; + + if (filter) { + StringBuffer sb = new StringBuffer(); + for (int i = start + 1; i < currentPosition - 1; i++) { + c = str.charAt(i); + if (c != '\\') + sb.append(c); + } + s = sb.toString(); + } else + s = str.substring(start + 1, currentPosition - 1); + return s; + } + } + } else if (singles.indexOf(c) >= 0) { + currentPosition++; + } else { + while ((currentPosition < maxPosition) && + singles.indexOf(str.charAt(currentPosition)) < 0 && + !Character.isWhitespace(str.charAt(currentPosition))) { + currentPosition++; + } + } + return str.substring(start, currentPosition); + } + + public void pushToken(String token) { + stack.addElement(token); + } +} diff --git a/app/src/main/java/com/sun/mail/auth/MD4.java b/app/src/main/java/com/sun/mail/auth/MD4.java new file mode 100644 index 0000000000..566de43f1e --- /dev/null +++ b/app/src/main/java/com/sun/mail/auth/MD4.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 2005, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/* + * Copied from OpenJDK with permission. + */ + +package com.sun.mail.auth; + +import java.security.*; + +//import static sun.security.provider.ByteArrayAccess.*; + +/** + * The MD4 class is used to compute an MD4 message digest over a given + * buffer of bytes. It is an implementation of the RSA Data Security Inc + * MD4 algorithim as described in internet RFC 1320. + * + * @author Andreas Sterbenz + * @author Bill Shannon (adapted for Jakarta Mail) + */ +public final class MD4 { + + // state of this object + private final int[] state; + // temporary buffer, used by implCompress() + private final int[] x; + + // size of the input to the compression function in bytes + private static final int blockSize = 64; + + // buffer to store partial blocks, blockSize bytes large + private final byte[] buffer = new byte[blockSize]; + // offset into buffer + private int bufOfs; + + // number of bytes processed so far. + // also used as a flag to indicate reset status + // -1: need to call engineReset() before next call to update() + // 0: is already reset + private long bytesProcessed; + + // rotation constants + private static final int S11 = 3; + private static final int S12 = 7; + private static final int S13 = 11; + private static final int S14 = 19; + private static final int S21 = 3; + private static final int S22 = 5; + private static final int S23 = 9; + private static final int S24 = 13; + private static final int S31 = 3; + private static final int S32 = 9; + private static final int S33 = 11; + private static final int S34 = 15; + + private static final byte[] padding; + + static { + padding = new byte[136]; + padding[0] = (byte)0x80; + } + + /** + * Standard constructor, creates a new MD4 instance. + */ + public MD4() { + state = new int[4]; + x = new int[16]; + implReset(); + } + + /** + * Compute and return the message digest of the input byte array. + * + * @param in the input byte array + * @return the message digest byte array + */ + public byte[] digest(byte[] in) { + implReset(); + engineUpdate(in, 0, in.length); + byte[] out = new byte[16]; + implDigest(out, 0); + return out; + } + + /** + * Reset the state of this object. + */ + private void implReset() { + // Load magic initialization constants. + state[0] = 0x67452301; + state[1] = 0xefcdab89; + state[2] = 0x98badcfe; + state[3] = 0x10325476; + bufOfs = 0; + bytesProcessed = 0; + } + + /** + * Perform the final computations, any buffered bytes are added + * to the digest, the count is added to the digest, and the resulting + * digest is stored. + */ + private void implDigest(byte[] out, int ofs) { + long bitsProcessed = bytesProcessed << 3; + + int index = (int)bytesProcessed & 0x3f; + int padLen = (index < 56) ? (56 - index) : (120 - index); + engineUpdate(padding, 0, padLen); + + //i2bLittle4((int)bitsProcessed, buffer, 56); + //i2bLittle4((int)(bitsProcessed >>> 32), buffer, 60); + buffer[56] = (byte)bitsProcessed; + buffer[57] = (byte)(bitsProcessed>>8); + buffer[58] = (byte)(bitsProcessed>>16); + buffer[59] = (byte)(bitsProcessed>>24); + buffer[60] = (byte)(bitsProcessed>>32); + buffer[61] = (byte)(bitsProcessed>>40); + buffer[62] = (byte)(bitsProcessed>>48); + buffer[63] = (byte)(bitsProcessed>>56); + implCompress(buffer, 0); + + //i2bLittle(state, 0, out, ofs, 16); + for (int i = 0; i < state.length; i++) { + int x = state[i]; + out[ofs++] = (byte)x; + out[ofs++] = (byte)(x>>8); + out[ofs++] = (byte)(x>>16); + out[ofs++] = (byte)(x>>24); + } + } + + private void engineUpdate(byte[] b, int ofs, int len) { + if (len == 0) { + return; + } + if ((ofs < 0) || (len < 0) || (ofs > b.length - len)) { + throw new ArrayIndexOutOfBoundsException(); + } + if (bytesProcessed < 0) { + implReset(); + } + bytesProcessed += len; + // if buffer is not empty, we need to fill it before proceeding + if (bufOfs != 0) { + int n = Math.min(len, blockSize - bufOfs); + System.arraycopy(b, ofs, buffer, bufOfs, n); + bufOfs += n; + ofs += n; + len -= n; + if (bufOfs >= blockSize) { + // compress completed block now + implCompress(buffer, 0); + bufOfs = 0; + } + } + // compress complete blocks + while (len >= blockSize) { + implCompress(b, ofs); + len -= blockSize; + ofs += blockSize; + } + // copy remainder to buffer + if (len > 0) { + System.arraycopy(b, ofs, buffer, 0, len); + bufOfs = len; + } + } + + private static int FF(int a, int b, int c, int d, int x, int s) { + a += ((b & c) | ((~b) & d)) + x; + return ((a << s) | (a >>> (32 - s))); + } + + private static int GG(int a, int b, int c, int d, int x, int s) { + a += ((b & c) | (b & d) | (c & d)) + x + 0x5a827999; + return ((a << s) | (a >>> (32 - s))); + } + + private static int HH(int a, int b, int c, int d, int x, int s) { + a += ((b ^ c) ^ d) + x + 0x6ed9eba1; + return ((a << s) | (a >>> (32 - s))); + } + + /** + * This is where the functions come together as the generic MD4 + * transformation operation. It consumes 64 + * bytes from the buffer, beginning at the specified offset. + */ + private void implCompress(byte[] buf, int ofs) { + //b2iLittle64(buf, ofs, x); + for (int xfs = 0; xfs < x.length; xfs++) { + x[xfs] = (buf[ofs] & 0xff) | ((buf[ofs+1] & 0xff) << 8) | + ((buf[ofs+2] & 0xff) << 16) | ((buf[ofs+3] & 0xff) << 24); + ofs += 4; + } + + int a = state[0]; + int b = state[1]; + int c = state[2]; + int d = state[3]; + + /* Round 1 */ + a = FF (a, b, c, d, x[ 0], S11); /* 1 */ + d = FF (d, a, b, c, x[ 1], S12); /* 2 */ + c = FF (c, d, a, b, x[ 2], S13); /* 3 */ + b = FF (b, c, d, a, x[ 3], S14); /* 4 */ + a = FF (a, b, c, d, x[ 4], S11); /* 5 */ + d = FF (d, a, b, c, x[ 5], S12); /* 6 */ + c = FF (c, d, a, b, x[ 6], S13); /* 7 */ + b = FF (b, c, d, a, x[ 7], S14); /* 8 */ + a = FF (a, b, c, d, x[ 8], S11); /* 9 */ + d = FF (d, a, b, c, x[ 9], S12); /* 10 */ + c = FF (c, d, a, b, x[10], S13); /* 11 */ + b = FF (b, c, d, a, x[11], S14); /* 12 */ + a = FF (a, b, c, d, x[12], S11); /* 13 */ + d = FF (d, a, b, c, x[13], S12); /* 14 */ + c = FF (c, d, a, b, x[14], S13); /* 15 */ + b = FF (b, c, d, a, x[15], S14); /* 16 */ + + /* Round 2 */ + a = GG (a, b, c, d, x[ 0], S21); /* 17 */ + d = GG (d, a, b, c, x[ 4], S22); /* 18 */ + c = GG (c, d, a, b, x[ 8], S23); /* 19 */ + b = GG (b, c, d, a, x[12], S24); /* 20 */ + a = GG (a, b, c, d, x[ 1], S21); /* 21 */ + d = GG (d, a, b, c, x[ 5], S22); /* 22 */ + c = GG (c, d, a, b, x[ 9], S23); /* 23 */ + b = GG (b, c, d, a, x[13], S24); /* 24 */ + a = GG (a, b, c, d, x[ 2], S21); /* 25 */ + d = GG (d, a, b, c, x[ 6], S22); /* 26 */ + c = GG (c, d, a, b, x[10], S23); /* 27 */ + b = GG (b, c, d, a, x[14], S24); /* 28 */ + a = GG (a, b, c, d, x[ 3], S21); /* 29 */ + d = GG (d, a, b, c, x[ 7], S22); /* 30 */ + c = GG (c, d, a, b, x[11], S23); /* 31 */ + b = GG (b, c, d, a, x[15], S24); /* 32 */ + + /* Round 3 */ + a = HH (a, b, c, d, x[ 0], S31); /* 33 */ + d = HH (d, a, b, c, x[ 8], S32); /* 34 */ + c = HH (c, d, a, b, x[ 4], S33); /* 35 */ + b = HH (b, c, d, a, x[12], S34); /* 36 */ + a = HH (a, b, c, d, x[ 2], S31); /* 37 */ + d = HH (d, a, b, c, x[10], S32); /* 38 */ + c = HH (c, d, a, b, x[ 6], S33); /* 39 */ + b = HH (b, c, d, a, x[14], S34); /* 40 */ + a = HH (a, b, c, d, x[ 1], S31); /* 41 */ + d = HH (d, a, b, c, x[ 9], S32); /* 42 */ + c = HH (c, d, a, b, x[ 5], S33); /* 43 */ + b = HH (b, c, d, a, x[13], S34); /* 44 */ + a = HH (a, b, c, d, x[ 3], S31); /* 45 */ + d = HH (d, a, b, c, x[11], S32); /* 46 */ + c = HH (c, d, a, b, x[ 7], S33); /* 47 */ + b = HH (b, c, d, a, x[15], S34); /* 48 */ + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + } +} diff --git a/app/src/main/java/com/sun/mail/auth/Ntlm.java b/app/src/main/java/com/sun/mail/auth/Ntlm.java new file mode 100644 index 0000000000..882776fcca --- /dev/null +++ b/app/src/main/java/com/sun/mail/auth/Ntlm.java @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2005, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +/* + * Copied from OpenJDK with permission. + */ + +package com.sun.mail.auth; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.PrintStream; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import java.util.Random; +import java.util.logging.Level; +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.SecretKeySpec; + +import com.sun.mail.util.BASE64DecoderStream; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.MailLogger; + + +/** + * NTLMAuthentication: + * + * @author Michael McMahon + * @author Bill Shannon (adapted for Jakarta Mail) + */ +public class Ntlm { + + private byte[] type1; + private byte[] type3; + + private SecretKeyFactory fac; + private Cipher cipher; + private MD4 md4; + private String hostname; + private String ntdomain; + private String username; + private String password; + + private Mac hmac; + + private MailLogger logger; + + // NTLM flags, as defined in Microsoft NTLM spec + // https://msdn.microsoft.com/en-us/library/cc236621.aspx + private static final int NTLMSSP_NEGOTIATE_UNICODE = 0x00000001; + private static final int NTLMSSP_NEGOTIATE_OEM = 0x00000002; + private static final int NTLMSSP_REQUEST_TARGET = 0x00000004; + private static final int NTLMSSP_NEGOTIATE_SIGN = 0x00000010; + private static final int NTLMSSP_NEGOTIATE_SEAL = 0x00000020; + private static final int NTLMSSP_NEGOTIATE_DATAGRAM = 0x00000040; + private static final int NTLMSSP_NEGOTIATE_LM_KEY = 0x00000080; + private static final int NTLMSSP_NEGOTIATE_NTLM = 0x00000200; + private static final int NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED = 0x00001000; + private static final int NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED = 0x00002000; + private static final int NTLMSSP_NEGOTIATE_ALWAYS_SIGN = 0x00008000; + private static final int NTLMSSP_TARGET_TYPE_DOMAIN = 0x00010000; + private static final int NTLMSSP_TARGET_TYPE_SERVER = 0x00020000; + private static final int NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY = 0x00080000; + private static final int NTLMSSP_NEGOTIATE_IDENTIFY = 0x00100000; + private static final int NTLMSSP_REQUEST_NON_NT_SESSION_KEY = 0x00400000; + private static final int NTLMSSP_NEGOTIATE_TARGET_INFO = 0x00800000; + private static final int NTLMSSP_NEGOTIATE_VERSION = 0x02000000; + private static final int NTLMSSP_NEGOTIATE_128 = 0x20000000; + private static final int NTLMSSP_NEGOTIATE_KEY_EXCH = 0x40000000; + private static final int NTLMSSP_NEGOTIATE_56 = 0x80000000; + + private static final byte RESPONSERVERSION = 1; + private static final byte HIRESPONSERVERSION = 1; + private static final byte[] Z6 = new byte[] { 0, 0, 0, 0, 0, 0 }; + private static final byte[] Z4 = new byte[] { 0, 0, 0, 0 }; + + private void init0() { + type1 = new byte[256]; // hopefully large enough + type3 = new byte[512]; // ditto + System.arraycopy(new byte[] {'N','T','L','M','S','S','P',0,1}, 0, + type1, 0, 9); + System.arraycopy(new byte[] {'N','T','L','M','S','S','P',0,3}, 0, + type3, 0, 9); + + try { + fac = SecretKeyFactory.getInstance("DES"); + cipher = Cipher.getInstance("DES/ECB/NoPadding"); + md4 = new MD4(); + } catch (NoSuchPaddingException e) { + assert false; + } catch (NoSuchAlgorithmException e) { + assert false; + } + }; + + /** + * Create an NTLM authenticator. + * Username may be specified as domain\\username in the Authenticator. + * If this notation is not used, then the domain will be taken + * from the ntdomain parameter. + * + * @param ntdomain the NT domain + * @param hostname the host name + * @param username the user name + * @param password the password + * @param logger the MailLogger + */ + public Ntlm(String ntdomain, String hostname, String username, + String password, MailLogger logger) { + int i = hostname.indexOf('.'); + if (i != -1) { + hostname = hostname.substring(0, i); + } + i = username.indexOf('\\'); + if (i != -1) { + ntdomain = username.substring(0, i).toUpperCase(Locale.ENGLISH); + username = username.substring(i+1); + } else if (ntdomain == null) { + ntdomain = ""; + } + this.ntdomain = ntdomain; + this.hostname = hostname; + this.username = username; + this.password = password; + this.logger = logger.getLogger(this.getClass(), "DEBUG NTLM"); + init0(); + } + + private void copybytes(byte[] dest, int destpos, String src, String enc) { + try { + byte[] x = src.getBytes(enc); + System.arraycopy(x, 0, dest, destpos, x.length); + } catch (UnsupportedEncodingException e) { + assert false; + } + } + + // for compatibility, just in case + public String generateType1Msg(int flags) { + return generateType1Msg(flags, false); + } + + public String generateType1Msg(int flags, boolean v2) { + int dlen = ntdomain.length(); + int type1flags = + NTLMSSP_NEGOTIATE_UNICODE | + NTLMSSP_NEGOTIATE_OEM | + NTLMSSP_NEGOTIATE_NTLM | + NTLMSSP_NEGOTIATE_OEM_WORKSTATION_SUPPLIED | + NTLMSSP_NEGOTIATE_ALWAYS_SIGN | + flags; + if (dlen != 0) + type1flags |= NTLMSSP_NEGOTIATE_OEM_DOMAIN_SUPPLIED; + if (v2) + type1flags |= NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY; + writeInt(type1, 12, type1flags); + type1[28] = (byte) 0x20; // host name offset + writeShort(type1, 16, dlen); + writeShort(type1, 18, dlen); + + int hlen = hostname.length(); + writeShort(type1, 24, hlen); + writeShort(type1, 26, hlen); + + copybytes(type1, 32, hostname, "iso-8859-1"); + copybytes(type1, hlen+32, ntdomain, "iso-8859-1"); + writeInt(type1, 20, hlen+32); + + byte[] msg = new byte[32 + hlen + dlen]; + System.arraycopy(type1, 0, msg, 0, 32 + hlen + dlen); + if (logger.isLoggable(Level.FINE)) + logger.fine("type 1 message: " + toHex(msg)); + + String result = null; + try { + result = new String(BASE64EncoderStream.encode(msg), "iso-8859-1"); + } catch (UnsupportedEncodingException e) { + assert false; + } + return result; + } + + /** + * Convert a 7 byte array to an 8 byte array (for a des key with parity). + * Input starts at offset off. + */ + private byte[] makeDesKey(byte[] input, int off) { + int[] in = new int[input.length]; + for (int i = 0; i < in.length; i++) { + in[i] = input[i] < 0 ? input[i] + 256: input[i]; + } + byte[] out = new byte[8]; + out[0] = (byte)in[off+0]; + out[1] = (byte)(((in[off+0] << 7) & 0xFF) | (in[off+1] >> 1)); + out[2] = (byte)(((in[off+1] << 6) & 0xFF) | (in[off+2] >> 2)); + out[3] = (byte)(((in[off+2] << 5) & 0xFF) | (in[off+3] >> 3)); + out[4] = (byte)(((in[off+3] << 4) & 0xFF) | (in[off+4] >> 4)); + out[5] = (byte)(((in[off+4] << 3) & 0xFF) | (in[off+5] >> 5)); + out[6] = (byte)(((in[off+5] << 2) & 0xFF) | (in[off+6] >> 6)); + out[7] = (byte)((in[off+6] << 1) & 0xFF); + return out; + } + + /** + * Compute hash-based message authentication code for NTLMv2. + */ + private byte[] hmacMD5(byte[] key, byte[] text) { + try { + if (hmac == null) + hmac = Mac.getInstance("HmacMD5"); + } catch (NoSuchAlgorithmException ex) { + throw new AssertionError(); + } + try { + byte[] nk = new byte[16]; + System.arraycopy(key, 0, nk, 0, key.length > 16 ? 16 : key.length); + SecretKeySpec skey = new SecretKeySpec(nk, "HmacMD5"); + hmac.init(skey); + return hmac.doFinal(text); + } catch (InvalidKeyException ex) { + assert false; + } catch (RuntimeException e) { + assert false; + } + return null; + } + + private byte[] calcLMHash() throws GeneralSecurityException { + byte[] magic = {0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25}; + byte[] pwb = null; + try { + pwb = password.toUpperCase(Locale.ENGLISH).getBytes("iso-8859-1"); + } catch (UnsupportedEncodingException ex) { + // should never happen + assert false; + } + byte[] pwb1 = new byte[14]; + int len = password.length(); + if (len > 14) + len = 14; + System.arraycopy(pwb, 0, pwb1, 0, len); /* Zero padded */ + + DESKeySpec dks1 = new DESKeySpec(makeDesKey(pwb1, 0)); + DESKeySpec dks2 = new DESKeySpec(makeDesKey(pwb1, 7)); + + SecretKey key1 = fac.generateSecret(dks1); + SecretKey key2 = fac.generateSecret(dks2); + cipher.init(Cipher.ENCRYPT_MODE, key1); + byte[] out1 = cipher.doFinal(magic, 0, 8); + cipher.init(Cipher.ENCRYPT_MODE, key2); + byte[] out2 = cipher.doFinal(magic, 0, 8); + + byte[] result = new byte [21]; + System.arraycopy(out1, 0, result, 0, 8); + System.arraycopy(out2, 0, result, 8, 8); + return result; + } + + private byte[] calcNTHash() throws GeneralSecurityException { + byte[] pw = null; + try { + pw = password.getBytes("UnicodeLittleUnmarked"); + } catch (UnsupportedEncodingException e) { + assert false; + } + byte[] out = md4.digest(pw); + byte[] result = new byte[21]; + System.arraycopy(out, 0, result, 0, 16); + return result; + } + + /* + * Key is a 21 byte array. Split it into 3 7 byte chunks, + * convert each to 8 byte DES keys, encrypt the text arg with + * each key and return the three results in a sequential []. + */ + private byte[] calcResponse(byte[] key, byte[] text) + throws GeneralSecurityException { + assert key.length == 21; + DESKeySpec dks1 = new DESKeySpec(makeDesKey(key, 0)); + DESKeySpec dks2 = new DESKeySpec(makeDesKey(key, 7)); + DESKeySpec dks3 = new DESKeySpec(makeDesKey(key, 14)); + SecretKey key1 = fac.generateSecret(dks1); + SecretKey key2 = fac.generateSecret(dks2); + SecretKey key3 = fac.generateSecret(dks3); + cipher.init(Cipher.ENCRYPT_MODE, key1); + byte[] out1 = cipher.doFinal(text, 0, 8); + cipher.init(Cipher.ENCRYPT_MODE, key2); + byte[] out2 = cipher.doFinal(text, 0, 8); + cipher.init(Cipher.ENCRYPT_MODE, key3); + byte[] out3 = cipher.doFinal(text, 0, 8); + byte[] result = new byte[24]; + System.arraycopy(out1, 0, result, 0, 8); + System.arraycopy(out2, 0, result, 8, 8); + System.arraycopy(out3, 0, result, 16, 8); + return result; + } + + /* + * Calculate the NTLMv2 response based on the nthash, additional data, + * and the original challenge. + */ + private byte[] calcV2Response(byte[] nthash, byte[] blob, byte[] challenge) + throws GeneralSecurityException { + byte[] txt = null; + try { + txt = (username.toUpperCase(Locale.ENGLISH) + ntdomain). + getBytes("UnicodeLittleUnmarked"); + } catch (UnsupportedEncodingException ex) { + // should never happen + assert false; + } + byte[] ntlmv2hash = hmacMD5(nthash, txt); + byte[] cb = new byte[blob.length + 8]; + System.arraycopy(challenge, 0, cb, 0, 8); + System.arraycopy(blob, 0, cb, 8, blob.length); + byte[] result = new byte[blob.length + 16]; + System.arraycopy(hmacMD5(ntlmv2hash, cb), 0, result, 0, 16); + System.arraycopy(blob, 0, result, 16, blob.length); + return result; + } + + public String generateType3Msg(String type2msg) { + try { + + /* First decode the type2 message to get the server challenge */ + /* challenge is located at type2[24] for 8 bytes */ + byte[] type2 = null; + try { + type2 = BASE64DecoderStream.decode(type2msg.getBytes("us-ascii")); + } catch (UnsupportedEncodingException ex) { + // should never happen + assert false; + } + if (logger.isLoggable(Level.FINE)) + logger.fine("type 2 message: " + toHex(type2)); + + byte[] challenge = new byte[8]; + System.arraycopy(type2, 24, challenge, 0, 8); + + int type3flags = + NTLMSSP_NEGOTIATE_UNICODE | + NTLMSSP_NEGOTIATE_NTLM | + NTLMSSP_NEGOTIATE_ALWAYS_SIGN; + + int ulen = username.length()*2; + writeShort(type3, 36, ulen); + writeShort(type3, 38, ulen); + int dlen = ntdomain.length()*2; + writeShort(type3, 28, dlen); + writeShort(type3, 30, dlen); + int hlen = hostname.length()*2; + writeShort(type3, 44, hlen); + writeShort(type3, 46, hlen); + + int l = 64; + copybytes(type3, l, ntdomain, "UnicodeLittleUnmarked"); + writeInt(type3, 32, l); + l += dlen; + copybytes(type3, l, username, "UnicodeLittleUnmarked"); + writeInt(type3, 40, l); + l += ulen; + copybytes(type3, l, hostname, "UnicodeLittleUnmarked"); + writeInt(type3, 48, l); + l += hlen; + + byte[] msg = null; + byte[] lmresponse = null; + byte[] ntresponse = null; + int flags = readInt(type2, 20); + + // did the server agree to NTLMv2? + if ((flags & NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY) != 0) { + // yes, create an NTLMv2 response + logger.fine("Using NTLMv2"); + type3flags |= NTLMSSP_NEGOTIATE_EXTENDED_SESSIONSECURITY; + byte[] nonce = new byte[8]; + // XXX - allow user to specify Random instance via properties? + (new Random()).nextBytes(nonce); + byte[] nthash = calcNTHash(); + lmresponse = calcV2Response(nthash, nonce, challenge); + byte[] targetInfo = new byte[0]; + if ((flags & NTLMSSP_NEGOTIATE_TARGET_INFO) != 0) { + int tlen = readShort(type2, 40); + int toff = readInt(type2, 44); + targetInfo = new byte[tlen]; + System.arraycopy(type2, toff, targetInfo, 0, tlen); + } + byte[] blob = new byte[32 + targetInfo.length]; + blob[0] = RESPONSERVERSION; + blob[1] = HIRESPONSERVERSION; + System.arraycopy(Z6, 0, blob, 2, 6); + // convert time to NT format + long now = (System.currentTimeMillis() + 11644473600000L) * 10000L; + for (int i = 0; i < 8; i++) { + blob[8 + i] = (byte)(now & 0xff); + now >>= 8; + } + System.arraycopy(nonce, 0, blob, 16, 8); + System.arraycopy(Z4, 0, blob, 24, 4); + System.arraycopy(targetInfo, 0, blob, 28, targetInfo.length); + System.arraycopy(Z4, 0, blob, 28 + targetInfo.length, 4); + ntresponse = calcV2Response(nthash, blob, challenge); + } else { + byte[] lmhash = calcLMHash(); + lmresponse = calcResponse(lmhash, challenge); + byte[] nthash = calcNTHash(); + ntresponse = calcResponse(nthash, challenge); + } + System.arraycopy(lmresponse, 0, type3, l, lmresponse.length); + writeShort(type3, 12, lmresponse.length); + writeShort(type3, 14, lmresponse.length); + writeInt(type3, 16, l); + l += 24; + System.arraycopy(ntresponse, 0, type3, l, ntresponse.length); + writeShort(type3, 20, ntresponse.length); + writeShort(type3, 22, ntresponse.length); + writeInt(type3, 24, l); + l += ntresponse.length; + writeShort(type3, 56, l); + + msg = new byte[l]; + System.arraycopy(type3, 0, msg, 0, l); + + writeInt(type3, 60, type3flags); + + if (logger.isLoggable(Level.FINE)) + logger.fine("type 3 message: " + toHex(msg)); + + String result = null; + try { + result = new String(BASE64EncoderStream.encode(msg), "iso-8859-1"); + } catch (UnsupportedEncodingException e) { + assert false; + } + return result; + + } catch (GeneralSecurityException ex) { + // should never happen + logger.log(Level.FINE, "GeneralSecurityException", ex); + return ""; // will fail later + } + } + + private static int readShort(byte[] b, int off) { + return (((int)b[off]) & 0xff) | + ((((int)b[off+1]) & 0xff) << 8); + } + + private void writeShort(byte[] b, int off, int data) { + b[off] = (byte) (data & 0xff); + b[off+1] = (byte) ((data >> 8) & 0xff); + } + + private static int readInt(byte[] b, int off) { + return (((int)b[off]) & 0xff) | + ((((int)b[off+1]) & 0xff) << 8) | + ((((int)b[off+2]) & 0xff) << 16) | + ((((int)b[off+3]) & 0xff) << 24); + } + + private void writeInt(byte[] b, int off, int data) { + b[off] = (byte) (data & 0xff); + b[off+1] = (byte) ((data >> 8) & 0xff); + b[off+2] = (byte) ((data >> 16) & 0xff); + b[off+3] = (byte) ((data >> 24) & 0xff); + } + + private static char[] hex = + { '0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F' }; + + private static String toHex(byte[] b) { + StringBuilder sb = new StringBuilder(b.length * 3); + for (int i = 0; i < b.length; i++) + sb.append(hex[(b[i]>>4)&0xF]).append(hex[b[i]&0xF]).append(' '); + return sb.toString(); + } +} diff --git a/app/src/main/java/com/sun/mail/auth/package.html b/app/src/main/java/com/sun/mail/auth/package.html new file mode 100644 index 0000000000..476f2ee06b --- /dev/null +++ b/app/src/main/java/com/sun/mail/auth/package.html @@ -0,0 +1,33 @@ + + + + + + +com.sun.mail.auth package + + + +

+This package includes internal authentication support classes and +SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. +

+ + + diff --git a/app/src/main/java/com/sun/mail/handlers/handler_base.java b/app/src/main/java/com/sun/mail/handlers/handler_base.java new file mode 100644 index 0000000000..dae159fdd6 --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/handler_base.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.IOException; +import javax.activation.*; + +/** + * Base class for other DataContentHandlers. + */ +public abstract class handler_base implements DataContentHandler { + + /** + * Return an array of ActivationDataFlavors that we support. + * Usually there will be only one. + * + * @return array of ActivationDataFlavors that we support + */ + protected abstract ActivationDataFlavor[] getDataFlavors(); + + /** + * Given the flavor that matched, return the appropriate type of object. + * Usually there's only one flavor so just call getContent. + * + * @param aFlavor the ActivationDataFlavor + * @param ds DataSource containing the data + * @return the object + * @exception IOException for errors reading the data + */ + protected Object getData(ActivationDataFlavor aFlavor, DataSource ds) + throws IOException { + return getContent(ds); + } + + /** + * Return the DataFlavors for this DataContentHandler. + * + * @return The DataFlavors + */ + public ActivationDataFlavor[] getTransferDataFlavors() { + return getDataFlavors().clone(); + } + + /** + * Return the Transfer Data of type DataFlavor from InputStream. + * + * @param df The DataFlavor + * @param ds The DataSource corresponding to the data + * @return the object + * @exception IOException for errors reading the data + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws IOException { + ActivationDataFlavor[] adf = getDataFlavors(); + for (int i = 0; i < adf.length; i++) { + // use ActivationDataFlavor.equals, which properly + // ignores Content-Type parameters in comparison + if (adf[i].equals(df)) + return getData(adf[i], ds); + } + return null; + } +} diff --git a/app/src/main/java/com/sun/mail/handlers/message_rfc822.java b/app/src/main/java/com/sun/mail/handlers/message_rfc822.java new file mode 100644 index 0000000000..a242e6b3e7 --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/message_rfc822.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.*; +import java.util.Properties; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; + + +/** + * @author Christopher Cotton + */ + + +public class message_rfc822 extends handler_base { + + private static ActivationDataFlavor[] ourDataFlavor = { + new ActivationDataFlavor(Message.class, "message/rfc822", "Message") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return ourDataFlavor; + } + + /** + * Return the content. + */ + @Override + public Object getContent(DataSource ds) throws IOException { + // create a new MimeMessage + try { + Session session; + if (ds instanceof MessageAware) { + MessageContext mc = ((MessageAware)ds).getMessageContext(); + session = mc.getSession(); + } else { + // Hopefully a rare case. Also hopefully the application + // has created a default Session that can just be returned + // here. If not, the one we create here is better than + // nothing, but overall not a really good answer. + session = Session.getDefaultInstance(new Properties(), null); + } + return new MimeMessage(session, ds.getInputStream()); + } catch (MessagingException me) { + IOException ioex = + new IOException("Exception creating MimeMessage in " + + "message/rfc822 DataContentHandler"); + ioex.initCause(me); + throw ioex; + } + } + + /** + * Write the object as a byte stream. + */ + @Override + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (!(obj instanceof Message)) + throw new IOException("\"" + getDataFlavors()[0].getMimeType() + + "\" DataContentHandler requires Message object, " + + "was given object of type " + obj.getClass().toString() + + "; obj.cl " + obj.getClass().getClassLoader() + + ", Message.cl " + Message.class.getClassLoader()); + + // if the object is a message, we know how to write that out + Message m = (Message)obj; + try { + m.writeTo(os); + } catch (MessagingException me) { + IOException ioex = new IOException("Exception writing message"); + ioex.initCause(me); + throw ioex; + } + } +} diff --git a/app/src/main/java/com/sun/mail/handlers/multipart_mixed.java b/app/src/main/java/com/sun/mail/handlers/multipart_mixed.java new file mode 100644 index 0000000000..98c7f28357 --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/multipart_mixed.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.*; +import javax.activation.*; +import javax.mail.MessagingException; +import javax.mail.Multipart; +import javax.mail.internet.MimeMultipart; + + +public class multipart_mixed extends handler_base { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(Multipart.class, + "multipart/mixed", "Multipart") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } + + /** + * Return the content. + */ + @Override + public Object getContent(DataSource ds) throws IOException { + try { + return new MimeMultipart(ds); + } catch (MessagingException e) { + IOException ioex = + new IOException("Exception while constructing MimeMultipart"); + ioex.initCause(e); + throw ioex; + } + } + + /** + * Write the object to the output stream, using the specific MIME type. + */ + @Override + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (!(obj instanceof Multipart)) + throw new IOException("\"" + getDataFlavors()[0].getMimeType() + + "\" DataContentHandler requires Multipart object, " + + "was given object of type " + obj.getClass().toString() + + "; obj.cl " + obj.getClass().getClassLoader() + + ", Multipart.cl " + Multipart.class.getClassLoader()); + + try { + ((Multipart)obj).writeTo(os); + } catch (MessagingException e) { + IOException ioex = + new IOException("Exception writing Multipart"); + ioex.initCause(e); + throw ioex; + } + } +} diff --git a/app/src/main/java/com/sun/mail/handlers/package.html b/app/src/main/java/com/sun/mail/handlers/package.html new file mode 100644 index 0000000000..0aafa21aba --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/package.html @@ -0,0 +1,33 @@ + + + + + + +com.sun.mail.handlers package + + + +

+This package includes internal data handler support classes and +SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. +

+ + + diff --git a/app/src/main/java/com/sun/mail/handlers/text_html.java b/app/src/main/java/com/sun/mail/handlers/text_html.java new file mode 100644 index 0000000000..24ef058520 --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/text_html.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import javax.activation.ActivationDataFlavor; + +/** + * DataContentHandler for text/html. + * + */ +public class text_html extends text_plain { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(String.class, "text/html", "HTML String") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } +} diff --git a/app/src/main/java/com/sun/mail/handlers/text_plain.java b/app/src/main/java/com/sun/mail/handlers/text_plain.java new file mode 100644 index 0000000000..da0beabd02 --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/text_plain.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.*; +import javax.activation.*; +import javax.mail.internet.ContentType; +import javax.mail.internet.MimeUtility; + +/** + * DataContentHandler for text/plain. + * + */ +public class text_plain extends handler_base { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(String.class, "text/plain", "Text String") + }; + + /** + * An OuputStream wrapper that doesn't close the underlying stream. + */ + private static class NoCloseOutputStream extends FilterOutputStream { + public NoCloseOutputStream(OutputStream os) { + super(os); + } + + @Override + public void close() { + // do nothing + } + } + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } + + @Override + public Object getContent(DataSource ds) throws IOException { + String enc = null; + InputStreamReader is = null; + + try { + enc = getCharset(ds.getContentType()); + is = new InputStreamReader(ds.getInputStream(), enc); + } catch (IllegalArgumentException iex) { + /* + * An unknown charset of the form ISO-XXX-XXX will cause + * the JDK to throw an IllegalArgumentException. The + * JDK will attempt to create a classname using this string, + * but valid classnames must not contain the character '-', + * and this results in an IllegalArgumentException, rather than + * the expected UnsupportedEncodingException. Yikes. + */ + throw new UnsupportedEncodingException(enc); + } + + try { + int pos = 0; + int count; + char buf[] = new char[1024]; + + while ((count = is.read(buf, pos, buf.length - pos)) != -1) { + pos += count; + if (pos >= buf.length) { + int size = buf.length; + if (size < 256*1024) + size += size; + else + size += 256*1024; + char tbuf[] = new char[size]; + System.arraycopy(buf, 0, tbuf, 0, pos); + buf = tbuf; + } + } + return new String(buf, 0, pos); + } finally { + try { + is.close(); + } catch (IOException ex) { + // ignore it + } + } + } + + /** + * Write the object to the output stream, using the specified MIME type. + */ + @Override + public void writeTo(Object obj, String type, OutputStream os) + throws IOException { + if (!(obj instanceof String)) + throw new IOException("\"" + getDataFlavors()[0].getMimeType() + + "\" DataContentHandler requires String object, " + + "was given object of type " + obj.getClass().toString()); + + String enc = null; + OutputStreamWriter osw = null; + + try { + enc = getCharset(type); + osw = new OutputStreamWriter(new NoCloseOutputStream(os), enc); + } catch (IllegalArgumentException iex) { + /* + * An unknown charset of the form ISO-XXX-XXX will cause + * the JDK to throw an IllegalArgumentException. The + * JDK will attempt to create a classname using this string, + * but valid classnames must not contain the character '-', + * and this results in an IllegalArgumentException, rather than + * the expected UnsupportedEncodingException. Yikes. + */ + throw new UnsupportedEncodingException(enc); + } + + String s = (String)obj; + osw.write(s, 0, s.length()); + /* + * Have to call osw.close() instead of osw.flush() because + * some charset converts, such as the iso-2022-jp converter, + * don't output the "shift out" sequence unless they're closed. + * The NoCloseOutputStream wrapper prevents the underlying + * stream from being closed. + */ + osw.close(); + } + + private String getCharset(String type) { + try { + ContentType ct = new ContentType(type); + String charset = ct.getParameter("charset"); + if (charset == null) + // If the charset parameter is absent, use US-ASCII. + charset = "us-ascii"; + return MimeUtility.javaCharset(charset); + } catch (Exception ex) { + return null; + } + } +} diff --git a/app/src/main/java/com/sun/mail/handlers/text_xml.java b/app/src/main/java/com/sun/mail/handlers/text_xml.java new file mode 100644 index 0000000000..1f0301793c --- /dev/null +++ b/app/src/main/java/com/sun/mail/handlers/text_xml.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.handlers; + +import java.io.IOException; +import java.io.OutputStream; + +import javax.activation.ActivationDataFlavor; +import javax.activation.DataSource; +import javax.mail.internet.ContentType; +import javax.mail.internet.ParseException; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.TransformerException; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.stream.StreamSource; + +/** + * DataContentHandler for text/xml. + * + * @author Anil Vijendran + * @author Bill Shannon + */ +public class text_xml extends text_plain { + + private static final ActivationDataFlavor[] flavors = { + new ActivationDataFlavor(String.class, "text/xml", "XML String"), + new ActivationDataFlavor(String.class, "application/xml", "XML String"), + new ActivationDataFlavor(StreamSource.class, "text/xml", "XML"), + new ActivationDataFlavor(StreamSource.class, "application/xml", "XML") + }; + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return flavors; + } + + @Override + protected Object getData(ActivationDataFlavor aFlavor, DataSource ds) + throws IOException { + if (aFlavor.getRepresentationClass() == String.class) + return super.getContent(ds); + else if (aFlavor.getRepresentationClass() == StreamSource.class) + return new StreamSource(ds.getInputStream()); + else + return null; // XXX - should never happen + } + + /** + */ + @Override + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (!isXmlType(mimeType)) + throw new IOException( + "Invalid content type \"" + mimeType + "\" for text/xml DCH"); + if (obj instanceof String) { + super.writeTo(obj, mimeType, os); + return; + } + if (!(obj instanceof DataSource || obj instanceof Source)) { + throw new IOException("Invalid Object type = "+obj.getClass()+ + ". XmlDCH can only convert DataSource or Source to XML."); + } + + try { + Transformer transformer = + TransformerFactory.newInstance().newTransformer(); + StreamResult result = new StreamResult(os); + if (obj instanceof DataSource) { + // Streaming transform applies only to + // javax.xml.transform.StreamSource + transformer.transform( + new StreamSource(((DataSource)obj).getInputStream()), + result); + } else { + transformer.transform((Source)obj, result); + } + } catch (TransformerException ex) { + IOException ioex = new IOException( + "Unable to run the JAXP transformer on a stream " + + ex.getMessage()); + ioex.initCause(ex); + throw ioex; + } catch (RuntimeException ex) { + IOException ioex = new IOException( + "Unable to run the JAXP transformer on a stream " + + ex.getMessage()); + ioex.initCause(ex); + throw ioex; + } + } + + private boolean isXmlType(String type) { + try { + ContentType ct = new ContentType(type); + return ct.getSubType().equals("xml") && + (ct.getPrimaryType().equals("text") || + ct.getPrimaryType().equals("application")); + } catch (ParseException ex) { + return false; + } catch (RuntimeException ex) { + return false; + } + } +} diff --git a/app/src/main/java/com/sun/mail/iap/Argument.java b/app/src/main/java/com/sun/mail/iap/Argument.java new file mode 100644 index 0000000000..677c63dc52 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/Argument.java @@ -0,0 +1,431 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.util.List; +import java.util.ArrayList; +import java.io.*; +import java.nio.charset.Charset; + +import com.sun.mail.util.ASCIIUtility; + +/** + * @author John Mani + * @author Bill Shannon + */ + +public class Argument { + protected List items; + + /** + * Constructor + */ + public Argument() { + items = new ArrayList<>(1); + } + + /** + * Append the given Argument to this Argument. All items + * from the source argument are copied into this destination + * argument. + * + * @param arg the Argument to append + * @return this + */ + public Argument append(Argument arg) { + items.addAll(arg.items); + return this; + } + + /** + * Write out given string as an ASTRING, depending on the type + * of the characters inside the string. The string should + * contain only ASCII characters.

+ * + * XXX: Hmm .. this should really be called writeASCII() + * + * @param s String to write out + * @return this + */ + public Argument writeString(String s) { + items.add(new AString(ASCIIUtility.getBytes(s))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an ASTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @exception UnsupportedEncodingException for bad charset + */ + public Argument writeString(String s, String charset) + throws UnsupportedEncodingException { + if (charset == null) // convenience + writeString(s); + else + items.add(new AString(s.getBytes(charset))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an ASTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @since JavaMail 1.6.0 + */ + public Argument writeString(String s, Charset charset) { + if (charset == null) // convenience + writeString(s); + else + items.add(new AString(s.getBytes(charset))); + return this; + } + + /** + * Write out given string as an NSTRING, depending on the type + * of the characters inside the string. The string should + * contain only ASCII characters.

+ * + * @param s String to write out + * @return this + * @since JavaMail 1.5.1 + */ + public Argument writeNString(String s) { + if (s == null) + items.add(new NString(null)); + else + items.add(new NString(ASCIIUtility.getBytes(s))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an NSTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @exception UnsupportedEncodingException for bad charset + * @since JavaMail 1.5.1 + */ + public Argument writeNString(String s, String charset) + throws UnsupportedEncodingException { + if (s == null) + items.add(new NString(null)); + else if (charset == null) // convenience + writeString(s); + else + items.add(new NString(s.getBytes(charset))); + return this; + } + + /** + * Convert the given string into bytes in the specified + * charset, and write the bytes out as an NSTRING + * + * @param s String to write out + * @param charset the charset + * @return this + * @since JavaMail 1.6.0 + */ + public Argument writeNString(String s, Charset charset) { + if (s == null) + items.add(new NString(null)); + else if (charset == null) // convenience + writeString(s); + else + items.add(new NString(s.getBytes(charset))); + return this; + } + + /** + * Write out given byte[] as a Literal. + * @param b byte[] to write out + * @return this + */ + public Argument writeBytes(byte[] b) { + items.add(b); + return this; + } + + /** + * Write out given ByteArrayOutputStream as a Literal. + * @param b ByteArrayOutputStream to be written out. + * @return this + */ + public Argument writeBytes(ByteArrayOutputStream b) { + items.add(b); + return this; + } + + /** + * Write out given data as a literal. + * @param b Literal representing data to be written out. + * @return this + */ + public Argument writeBytes(Literal b) { + items.add(b); + return this; + } + + /** + * Write out given string as an Atom. Note that an Atom can contain only + * certain US-ASCII characters. No validation is done on the characters + * in the string. + * @param s String + * @return this + */ + public Argument writeAtom(String s) { + items.add(new Atom(s)); + return this; + } + + /** + * Write out number. + * @param i number + * @return this + */ + public Argument writeNumber(int i) { + items.add(Integer.valueOf(i)); + return this; + } + + /** + * Write out number. + * @param i number + * @return this + */ + public Argument writeNumber(long i) { + items.add(Long.valueOf(i)); + return this; + } + + /** + * Write out as parenthesised list. + * + * @param c the Argument + * @return this + */ + public Argument writeArgument(Argument c) { + items.add(c); + return this; + } + + /* + * Write out all the buffered items into the output stream. + */ + public void write(Protocol protocol) + throws IOException, ProtocolException { + int size = items != null ? items.size() : 0; + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + + for (int i=0; i < size; i++) { + if (i > 0) // write delimiter if not the first item + os.write(' '); + + Object o = items.get(i); + if (o instanceof Atom) { + os.writeBytes(((Atom)o).string); + } else if (o instanceof Number) { + os.writeBytes(((Number)o).toString()); + } else if (o instanceof AString) { + astring(((AString)o).bytes, protocol); + } else if (o instanceof NString) { + nstring(((NString)o).bytes, protocol); + } else if (o instanceof byte[]) { + literal((byte[])o, protocol); + } else if (o instanceof ByteArrayOutputStream) { + literal((ByteArrayOutputStream)o, protocol); + } else if (o instanceof Literal) { + literal((Literal)o, protocol); + } else if (o instanceof Argument) { + os.write('('); // open parans + ((Argument)o).write(protocol); + os.write(')'); // close parans + } + } + } + + /** + * Write out given String as either an Atom, QuotedString or Literal + */ + private void astring(byte[] bytes, Protocol protocol) + throws IOException, ProtocolException { + nastring(bytes, protocol, false); + } + + /** + * Write out given String as either NIL, QuotedString, or Literal. + */ + private void nstring(byte[] bytes, Protocol protocol) + throws IOException, ProtocolException { + if (bytes == null) { + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + os.writeBytes("NIL"); + } else + nastring(bytes, protocol, true); + } + + private void nastring(byte[] bytes, Protocol protocol, boolean doQuote) + throws IOException, ProtocolException { + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + int len = bytes.length; + + // If length is greater than 1024 bytes, send as literal + if (len > 1024) { + literal(bytes, protocol); + return; + } + + // if 0 length, send as quoted-string + boolean quote = len == 0 ? true : doQuote; + boolean escape = false; + boolean utf8 = protocol.supportsUtf8(); + + byte b; + for (int i = 0; i < len; i++) { + b = bytes[i]; + if (b == '\0' || b == '\r' || b == '\n' || + (!utf8 && ((b & 0xff) > 0177))) { + // NUL, CR or LF means the bytes need to be sent as literals + literal(bytes, protocol); + return; + } + if (b == '*' || b == '%' || b == '(' || b == ')' || b == '{' || + b == '"' || b == '\\' || + ((b & 0xff) <= ' ') || ((b & 0xff) > 0177)) { + quote = true; + if (b == '"' || b == '\\') // need to escape these characters + escape = true; + } + } + + /* + * Make sure the (case-independent) string "NIL" is always quoted, + * so as not to be confused with a real NIL (handled above in nstring). + * This is more than is necessary, but it's rare to begin with and + * this makes it safer than doing the test in nstring above in case + * some code calls writeString when it should call writeNString. + */ + if (!quote && bytes.length == 3 && + (bytes[0] == 'N' || bytes[0] == 'n') && + (bytes[1] == 'I' || bytes[1] == 'i') && + (bytes[2] == 'L' || bytes[2] == 'l')) + quote = true; + + if (quote) // start quote + os.write('"'); + + if (escape) { + // already quoted + for (int i = 0; i < len; i++) { + b = bytes[i]; + if (b == '"' || b == '\\') + os.write('\\'); + os.write(b); + } + } else + os.write(bytes); + + + if (quote) // end quote + os.write('"'); + } + + /** + * Write out given byte[] as a literal + */ + private void literal(byte[] b, Protocol protocol) + throws IOException, ProtocolException { + startLiteral(protocol, b.length).write(b); + } + + /** + * Write out given ByteArrayOutputStream as a literal. + */ + private void literal(ByteArrayOutputStream b, Protocol protocol) + throws IOException, ProtocolException { + b.writeTo(startLiteral(protocol, b.size())); + } + + /** + * Write out given Literal as a literal. + */ + private void literal(Literal b, Protocol protocol) + throws IOException, ProtocolException { + b.writeTo(startLiteral(protocol, b.size())); + } + + private OutputStream startLiteral(Protocol protocol, int size) + throws IOException, ProtocolException { + DataOutputStream os = (DataOutputStream)protocol.getOutputStream(); + boolean nonSync = protocol.supportsNonSyncLiterals(); + + os.write('{'); + os.writeBytes(Integer.toString(size)); + if (nonSync) // server supports non-sync literals + os.writeBytes("+}\r\n"); + else + os.writeBytes("}\r\n"); + os.flush(); + + // If we are using synchronized literals, wait for the server's + // continuation signal + if (!nonSync) { + for (; ;) { + Response r = protocol.readResponse(); + if (r.isContinuation()) + break; + if (r.isTagged()) + throw new LiteralException(r); + // XXX - throw away untagged responses; + // violates IMAP spec, hope no servers do this + } + } + return os; + } +} + +class Atom { + String string; + + Atom(String s) { + string = s; + } +} + +class AString { + byte[] bytes; + + AString(byte[] b) { + bytes = b; + } +} + +class NString { + byte[] bytes; + + NString(byte[] b) { + bytes = b; + } +} diff --git a/app/src/main/java/com/sun/mail/iap/BadCommandException.java b/app/src/main/java/com/sun/mail/iap/BadCommandException.java new file mode 100644 index 0000000000..4a1b33bf6e --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/BadCommandException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class BadCommandException extends ProtocolException { + + private static final long serialVersionUID = 5769722539397237515L; + + /** + * Constructs an BadCommandException with no detail message. + */ + public BadCommandException() { + super(); + } + + /** + * Constructs an BadCommandException with the specified detail message. + * @param s the detail message + */ + public BadCommandException(String s) { + super(s); + } + + /** + * Constructs an BadCommandException with the specified Response. + * @param r the Response + */ + public BadCommandException(Response r) { + super(r); + } +} diff --git a/app/src/main/java/com/sun/mail/iap/ByteArray.java b/app/src/main/java/com/sun/mail/iap/ByteArray.java new file mode 100644 index 0000000000..e05110bcaf --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ByteArray.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.ByteArrayInputStream; + +/** + * A simple wrapper around a byte array, with a start position and + * count of bytes. + * + * @author John Mani + */ + +public class ByteArray { + private byte[] bytes; // the byte array + private int start; // start position + private int count; // count of bytes + + /** + * Constructor + * + * @param b the byte array to wrap + * @param start start position in byte array + * @param count number of bytes in byte array + */ + public ByteArray(byte[] b, int start, int count) { + bytes = b; + this.start = start; + this.count = count; + } + + /** + * Constructor that creates a byte array of the specified size. + * + * @param size the size of the ByteArray + * @since JavaMail 1.4.1 + */ + public ByteArray(int size) { + this(new byte[size], 0, size); + } + + /** + * Returns the internal byte array. Note that this is a live + * reference to the actual data, not a copy. + * + * @return the wrapped byte array + */ + public byte[] getBytes() { + return bytes; + } + + /** + * Returns a new byte array that is a copy of the data. + * + * @return a new byte array with the bytes from start for count + */ + public byte[] getNewBytes() { + byte[] b = new byte[count]; + System.arraycopy(bytes, start, b, 0, count); + return b; + } + + /** + * Returns the start position + * + * @return the start position + */ + public int getStart() { + return start; + } + + /** + * Returns the count of bytes + * + * @return the number of bytes + */ + public int getCount() { + return count; + } + + /** + * Set the count of bytes. + * + * @param count the number of bytes + * @since JavaMail 1.4.1 + */ + public void setCount(int count) { + this.count = count; + } + + /** + * Returns a ByteArrayInputStream. + * + * @return the ByteArrayInputStream + */ + public ByteArrayInputStream toByteArrayInputStream() { + return new ByteArrayInputStream(bytes, start, count); + } + + /** + * Grow the byte array by incr bytes. + * + * @param incr how much to grow + * @since JavaMail 1.4.1 + */ + public void grow(int incr) { + byte[] nbuf = new byte[bytes.length + incr]; + System.arraycopy(bytes, 0, nbuf, 0, bytes.length); + bytes = nbuf; + } +} diff --git a/app/src/main/java/com/sun/mail/iap/CommandFailedException.java b/app/src/main/java/com/sun/mail/iap/CommandFailedException.java new file mode 100644 index 0000000000..8c141e7ba5 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/CommandFailedException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class CommandFailedException extends ProtocolException { + + private static final long serialVersionUID = 793932807880443631L; + + /** + * Constructs an CommandFailedException with no detail message. + */ + public CommandFailedException() { + super(); + } + + /** + * Constructs an CommandFailedException with the specified detail message. + * @param s the detail message + */ + public CommandFailedException(String s) { + super(s); + } + + /** + * Constructs an CommandFailedException with the specified Response. + * @param r the Response. + */ + public CommandFailedException(Response r) { + super(r); + } +} diff --git a/app/src/main/java/com/sun/mail/iap/ConnectionException.java b/app/src/main/java/com/sun/mail/iap/ConnectionException.java new file mode 100644 index 0000000000..12dd496106 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ConnectionException.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class ConnectionException extends ProtocolException { + private transient Protocol p; + + private static final long serialVersionUID = 5749739604257464727L; + + /** + * Constructs an ConnectionException with no detail message. + */ + public ConnectionException() { + super(); + } + + /** + * Constructs an ConnectionException with the specified detail message. + * + * @param s the detail message + */ + public ConnectionException(String s) { + super(s); + } + + /** + * Constructs an ConnectionException with the specified Response. + * + * @param p the Protocol object + * @param r the Response + */ + public ConnectionException(Protocol p, Response r) { + super(r); + this.p = p; + } + + public Protocol getProtocol() { + return p; + } +} diff --git a/app/src/main/java/com/sun/mail/iap/Literal.java b/app/src/main/java/com/sun/mail/iap/Literal.java new file mode 100644 index 0000000000..14c592b274 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/Literal.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.*; + +/** + * An interface for objects that provide data dynamically for use in + * a literal protocol element. + * + * @author Bill Shannon + */ + +public interface Literal { + /** + * Return the size of the data. + * + * @return the size of the data + */ + public int size(); + + /** + * Write the data to the OutputStream. + * + * @param os the output stream + * @exception IOException for I/O errors + */ + public void writeTo(OutputStream os) throws IOException; +} diff --git a/app/src/main/java/com/sun/mail/iap/LiteralException.java b/app/src/main/java/com/sun/mail/iap/LiteralException.java new file mode 100644 index 0000000000..660747f442 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/LiteralException.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author Bill Shannon + */ + +public class LiteralException extends ProtocolException { + + private static final long serialVersionUID = -6919179828339609913L; + + /** + * Constructs a LiteralException with the specified Response object. + * + * @param r the response object + */ + public LiteralException(Response r) { + super(r.toString()); + response = r; + } +} diff --git a/app/src/main/java/com/sun/mail/iap/ParsingException.java b/app/src/main/java/com/sun/mail/iap/ParsingException.java new file mode 100644 index 0000000000..a1e7168a8f --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ParsingException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class ParsingException extends ProtocolException { + + private static final long serialVersionUID = 7756119840142724839L; + + /** + * Constructs an ParsingException with no detail message. + */ + public ParsingException() { + super(); + } + + /** + * Constructs an ParsingException with the specified detail message. + * @param s the detail message + */ + public ParsingException(String s) { + super(s); + } + + /** + * Constructs an ParsingException with the specified Response. + * @param r the Response + */ + public ParsingException(Response r) { + super(r); + } +} diff --git a/app/src/main/java/com/sun/mail/iap/Protocol.java b/app/src/main/java/com/sun/mail/iap/Protocol.java new file mode 100644 index 0000000000..b7bd688248 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/Protocol.java @@ -0,0 +1,682 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.util.Properties; +import java.io.*; +import java.nio.channels.SocketChannel; +import java.net.*; +import javax.net.ssl.SSLSocket; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketFetcher; +import com.sun.mail.util.TraceInputStream; +import com.sun.mail.util.TraceOutputStream; + +/** + * General protocol handling code for IMAP-like protocols.

+ * + * The Protocol object is multithread safe. + * + * @author John Mani + * @author Max Spivak + * @author Bill Shannon + */ + +public class Protocol { + protected String host; + private Socket socket; + // in case we turn on TLS, we'll need these later + protected boolean quote; + protected MailLogger logger; + protected MailLogger traceLogger; + protected Properties props; + protected String prefix; + + private TraceInputStream traceInput; // the Tracer + private volatile ResponseInputStream input; + + private TraceOutputStream traceOutput; // the Tracer + private volatile DataOutputStream output; + + private int tagCounter = 0; + private final String tagPrefix; + + private String localHostName; + + private final List handlers + = new CopyOnWriteArrayList<>(); + + private volatile long timestamp; + + // package private, to allow testing + static final AtomicInteger tagNum = new AtomicInteger(); + + private static final byte[] CRLF = { (byte)'\r', (byte)'\n'}; + + /** + * Constructor.

+ * + * Opens a connection to the given host at given port. + * + * @param host host to connect to + * @param port portnumber to connect to + * @param props Properties object used by this protocol + * @param prefix Prefix to prepend to property keys + * @param isSSL use SSL? + * @param logger log messages here + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public Protocol(String host, int port, + Properties props, String prefix, + boolean isSSL, MailLogger logger) + throws IOException, ProtocolException { + boolean connected = false; // did constructor succeed? + tagPrefix = computePrefix(props, prefix); + try { + this.host = host; + this.props = props; + this.prefix = prefix; + this.logger = logger; + traceLogger = logger.getSubLogger("protocol", null); + + socket = SocketFetcher.getSocket(host, port, props, prefix, isSSL); + quote = PropUtil.getBooleanProperty(props, + "mail.debug.quote", false); + + initStreams(); + + // Read server greeting + processGreeting(readResponse()); + + timestamp = System.currentTimeMillis(); + + connected = true; // must be last statement in constructor + } finally { + /* + * If we get here because an exception was thrown, we need + * to disconnect to avoid leaving a connected socket that + * no one will be able to use because this object was never + * completely constructed. + */ + if (!connected) + disconnect(); + } + } + + private void initStreams() throws IOException { + traceInput = new TraceInputStream(socket.getInputStream(), traceLogger); + traceInput.setQuote(quote); + input = new ResponseInputStream(traceInput); + + traceOutput = + new TraceOutputStream(socket.getOutputStream(), traceLogger); + traceOutput.setQuote(quote); + output = new DataOutputStream(new BufferedOutputStream(traceOutput)); + } + + /** + * Compute the tag prefix to be used for this connection. + * Start with "A" - "Z", then "AA" - "ZZ", and finally "AAA" - "ZZZ". + * Wrap around after that. + */ + private String computePrefix(Properties props, String prefix) { + // XXX - in case someone depends on the tag prefix + if (PropUtil.getBooleanProperty(props, + prefix + ".reusetagprefix", false)) + return "A"; + // tag prefix, wrap around after three letters + int n = tagNum.getAndIncrement() % (26*26*26 + 26*26 + 26); + String tagPrefix; + if (n < 26) + tagPrefix = new String(new char[] { (char)('A' + n) }); + else if (n < (26*26 + 26)) { + n -= 26; + tagPrefix = new String(new char[] { + (char)('A' + n/26), (char)('A' + n%26) }); + } else { + n -= (26*26 + 26); + tagPrefix = new String(new char[] { + (char)('A' + n/(26*26)), + (char)('A' + (n%(26*26))/26), + (char)('A' + n%26) }); + } + return tagPrefix; + } + + /** + * Constructor for debugging. + * + * @param in the InputStream to read from + * @param out the PrintStream to write to + * @param props Properties object used by this protocol + * @param debug true to enable debugging output + * @exception IOException for I/O errors + */ + public Protocol(InputStream in, PrintStream out, Properties props, + boolean debug) throws IOException { + this.host = "localhost"; + this.props = props; + this.quote = false; + tagPrefix = computePrefix(props, "mail.imap"); + logger = new MailLogger(this.getClass(), "DEBUG", debug, System.out); + traceLogger = logger.getSubLogger("protocol", null); + + // XXX - inlined initStreams, won't allow later startTLS + traceInput = new TraceInputStream(in, traceLogger); + traceInput.setQuote(quote); + input = new ResponseInputStream(traceInput); + + traceOutput = new TraceOutputStream(out, traceLogger); + traceOutput.setQuote(quote); + output = new DataOutputStream(new BufferedOutputStream(traceOutput)); + + timestamp = System.currentTimeMillis(); + } + + /** + * Returns the timestamp. + * + * @return the timestamp + */ + public long getTimestamp() { + return timestamp; + } + + /** + * Adds a response handler. + * + * @param h the response handler + */ + public void addResponseHandler(ResponseHandler h) { + handlers.add(h); + } + + /** + * Removed the specified response handler. + * + * @param h the response handler + */ + public void removeResponseHandler(ResponseHandler h) { + handlers.remove(h); + } + + /** + * Notify response handlers + * + * @param responses the responses + */ + public void notifyResponseHandlers(Response[] responses) { + if (handlers.isEmpty()) { + return; + } + + for (Response r : responses) { + if (r != null) { + for (ResponseHandler rh : handlers) { + if (rh != null) { + rh.handleResponse(r); + } + } + } + } + } + + protected void processGreeting(Response r) throws ProtocolException { + if (r.isBYE()) + throw new ConnectionException(this, r); + } + + /** + * Return the Protocol's InputStream. + * + * @return the input stream + */ + protected ResponseInputStream getInputStream() { + return input; + } + + /** + * Return the Protocol's OutputStream + * + * @return the output stream + */ + protected OutputStream getOutputStream() { + return output; + } + + /** + * Returns whether this Protocol supports non-synchronizing literals + * Default is false. Subclasses should override this if required + * + * @return true if the server supports non-synchronizing literals + */ + protected synchronized boolean supportsNonSyncLiterals() { + return false; + } + + public Response readResponse() + throws IOException, ProtocolException { + return new Response(this); + } + + /** + * Is another response available in our buffer? + * + * @return true if another response is in the buffer + * @since JavaMail 1.5.4 + */ + public boolean hasResponse() { + /* + * XXX - Really should peek ahead in the buffer to see + * if there's a *complete* response available, but if there + * isn't who's going to read more data into the buffer + * until there is? + */ + try { + return input.available() > 0; + } catch (IOException ex) { + } + return false; + } + + /** + * Return a buffer to be used to read a response. + * The default implementation returns null, which causes + * a new buffer to be allocated for every response. + * + * @return the buffer to use + * @since JavaMail 1.4.1 + */ + protected ByteArray getResponseBuffer() { + return null; + } + + public String writeCommand(String command, Argument args) + throws IOException, ProtocolException { + // assert Thread.holdsLock(this); + // can't assert because it's called from constructor + String tag = tagPrefix + Integer.toString(tagCounter++); // unique tag + + output.writeBytes(tag + " " + command); + + if (args != null) { + output.write(' '); + args.write(this); + } + + output.write(CRLF); + output.flush(); + return tag; + } + + /** + * Send a command to the server. Collect all responses until either + * the corresponding command completion response or a BYE response + * (indicating server failure). Return all the collected responses. + * + * @param command the command + * @param args the arguments + * @return array of Response objects returned by the server + */ + public synchronized Response[] command(String command, Argument args) { + commandStart(command); + List v = new ArrayList<>(); + boolean done = false; + String tag = null; + + // write the command + try { + tag = writeCommand(command, args); + } catch (LiteralException lex) { + v.add(lex.getResponse()); + done = true; + } catch (Exception ex) { + // Convert this into a BYE response + v.add(Response.byeResponse(ex)); + done = true; + } + + Response byeResp = null; + while (!done) { + Response r = null; + try { + r = readResponse(); + } catch (IOException ioex) { + if (byeResp == null) // convert this into a BYE response + byeResp = Response.byeResponse(ioex); + // else, connection closed after BYE was sent + break; + } catch (ProtocolException pex) { + logger.log(Level.FINE, "ignoring bad response", pex); + continue; // skip this response + } + + if (r.isBYE()) { + byeResp = r; + continue; + } + + v.add(r); + + // If this is a matching command completion response, we are done + if (r.isTagged() && r.getTag().equals(tag)) + done = true; + } + + if (byeResp != null) + v.add(byeResp); // must be last + Response[] responses = new Response[v.size()]; + v.toArray(responses); + timestamp = System.currentTimeMillis(); + commandEnd(); + return responses; + } + + /** + * Convenience routine to handle OK, NO, BAD and BYE responses. + * + * @param response the response + * @exception ProtocolException for protocol failures + */ + public void handleResult(Response response) throws ProtocolException { + if (response.isOK()) + return; + else if (response.isNO()) + throw new CommandFailedException(response); + else if (response.isBAD()) + throw new BadCommandException(response); + else if (response.isBYE()) { + disconnect(); + throw new ConnectionException(this, response); + } + } + + /** + * Convenience routine to handle simple IAP commands + * that do not have responses specific to that command. + * + * @param cmd the command + * @param args the arguments + * @exception ProtocolException for protocol failures + */ + public void simpleCommand(String cmd, Argument args) + throws ProtocolException { + // Issue command + Response[] r = command(cmd, args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + } + + /** + * Start TLS on the current connection. + * cmd is the command to issue to start TLS negotiation. + * If the command succeeds, we begin TLS negotiation. + * If the socket is already an SSLSocket this is a nop and the command + * is not issued. + * + * @param cmd the command to issue + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public synchronized void startTLS(String cmd) + throws IOException, ProtocolException { + if (socket instanceof SSLSocket) + return; // nothing to do + simpleCommand(cmd, null); + socket = SocketFetcher.startTLS(socket, host, props, prefix); + initStreams(); + } + + /** + * Start compression on the current connection. + * cmd is the command to issue to start compression. + * If the command succeeds, we begin compression. + * + * @param cmd the command to issue + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public synchronized void startCompression(String cmd) + throws IOException, ProtocolException { + // XXX - check whether compression is already enabled? + simpleCommand(cmd, null); + + // need to create our own Inflater and Deflater in order to set nowrap + Inflater inf = new Inflater(true); + traceInput = new TraceInputStream(new InflaterInputStream( + socket.getInputStream(), inf), traceLogger); + traceInput.setQuote(quote); + input = new ResponseInputStream(traceInput); + + // configure the Deflater + int level = PropUtil.getIntProperty(props, prefix + ".compress.level", + Deflater.DEFAULT_COMPRESSION); + int strategy = PropUtil.getIntProperty(props, + prefix + ".compress.strategy", + Deflater.DEFAULT_STRATEGY); + if (logger.isLoggable(Level.FINE)) + logger.log(Level.FINE, + "Creating Deflater with compression level {0} and strategy {1}", + new Object[] { level, strategy }); + Deflater def = new Deflater(Deflater.DEFAULT_COMPRESSION, true); + try { + def.setLevel(level); + } catch (IllegalArgumentException ex) { + logger.log(Level.FINE, "Ignoring bad compression level", ex); + } + try { + def.setStrategy(strategy); + } catch (IllegalArgumentException ex) { + logger.log(Level.FINE, "Ignoring bad compression strategy", ex); + } + traceOutput = new TraceOutputStream(new DeflaterOutputStream( + socket.getOutputStream(), def, true), traceLogger); + traceOutput.setQuote(quote); + output = new DataOutputStream(new BufferedOutputStream(traceOutput)); + } + + /** + * Is this connection using an SSL socket? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public boolean isSSL() { + return socket instanceof SSLSocket; + } + + /** + * Return the address the socket connected to. + * + * @return the InetAddress the socket is connected to + * @since JavaMail 1.5.2 + */ + public InetAddress getInetAddress() { + return socket.getInetAddress(); + } + + /** + * Return the SocketChannel associated with this connection, if any. + * + * @return the SocketChannel + * @since JavaMail 1.5.2 + */ + public SocketChannel getChannel() { + SocketChannel ret = socket.getChannel(); + if (ret != null) + return ret; + + // XXX - Android is broken and SSL wrapped sockets don't delegate + // the getChannel method to the wrapped Socket + if (socket instanceof SSLSocket) { + try { + Field f = socket.getClass().getDeclaredField("socket"); + f.setAccessible(true); + Socket s = (Socket)f.get(socket); + ret = s.getChannel(); + } catch (Exception ex) { + // ignore anything that might go wrong + } + } + return ret; + } + + /** + * Return the local SocketAddress (host and port) for this + * end of the connection. + * + * @return the SocketAddress + * @since Jakarta Mail 1.6.4 + */ + public SocketAddress getLocalSocketAddress() { + return socket.getLocalSocketAddress(); + } + + /** + * Does the server support UTF-8? + * This implementation returns false. + * Subclasses should override as appropriate. + * + * @return true if the server supports UTF-8 + * @since JavaMail 1.6.0 + */ + public boolean supportsUtf8() { + return false; + } + + /** + * Disconnect. + */ + protected synchronized void disconnect() { + if (socket != null) { + try { + socket.close(); + } catch (IOException e) { + // ignore it + } + socket = null; + } + } + + /** + * Get the name of the local host. + * The property <prefix>.localhost overrides + * <prefix>.localaddress, + * which overrides what InetAddress would tell us. + * + * @return the name of the local host + */ + protected synchronized String getLocalHost() { + // get our hostname and cache it for future use + if (localHostName == null || localHostName.length() <= 0) + localHostName = + props.getProperty(prefix + ".localhost"); + if (localHostName == null || localHostName.length() <= 0) + localHostName = + props.getProperty(prefix + ".localaddress"); + try { + if (localHostName == null || localHostName.length() <= 0) { + InetAddress localHost = InetAddress.getLocalHost(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } catch (UnknownHostException uhex) { + } + + // last chance, try to get our address from our socket + if (localHostName == null || localHostName.length() <= 0) { + if (socket != null && socket.isBound()) { + InetAddress localHost = socket.getLocalAddress(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } + return localHostName; + } + + /** + * Is protocol tracing enabled? + * + * @return true if protocol tracing is enabled + */ + protected boolean isTracing() { + return traceLogger.isLoggable(Level.FINEST); + } + + /** + * Temporarily turn off protocol tracing, e.g., to prevent + * tracing the authentication sequence, including the password. + */ + protected void suspendTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(false); + traceOutput.setTrace(false); + } + } + + /** + * Resume protocol tracing, if it was enabled to begin with. + */ + protected void resumeTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(true); + traceOutput.setTrace(true); + } + } + + /** + * Finalizer. + */ + @Override + protected void finalize() throws Throwable { + try { + disconnect(); + } finally { + super.finalize(); + } + } + + /* + * Probe points for GlassFish monitoring. + */ + private void commandStart(String command) { } + private void commandEnd() { } +} diff --git a/app/src/main/java/com/sun/mail/iap/ProtocolException.java b/app/src/main/java/com/sun/mail/iap/ProtocolException.java new file mode 100644 index 0000000000..f4d9e3a5d0 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ProtocolException.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * @author John Mani + */ + +public class ProtocolException extends Exception { + protected transient Response response = null; + + private static final long serialVersionUID = -4360500807971797439L; + + /** + * Constructs a ProtocolException with no detail message. + */ + public ProtocolException() { + super(); + } + + /** + * Constructs a ProtocolException with the specified detail message. + * + * @param message the detail message + */ + public ProtocolException(String message) { + super(message); + } + + /** + * Constructs a ProtocolException with the specified detail message + * and cause. + * + * @param message the detail message + * @param cause the cause + */ + public ProtocolException(String message, Throwable cause) { + super(message, cause); + } + + /** + * Constructs a ProtocolException with the specified Response object. + * + * @param r the Response + */ + public ProtocolException(Response r) { + super(r.toString()); + response = r; + } + + /** + * Return the offending Response object. + * + * @return the Response object + */ + public Response getResponse() { + return response; + } +} diff --git a/app/src/main/java/com/sun/mail/iap/Response.java b/app/src/main/java/com/sun/mail/iap/Response.java new file mode 100644 index 0000000000..fcc4369957 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/Response.java @@ -0,0 +1,600 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.*; +import java.util.*; +import java.nio.charset.StandardCharsets; + +import com.sun.mail.util.ASCIIUtility; + +/** + * This class represents a response obtained from the input stream + * of an IMAP server. + * + * @author John Mani + * @author Bill Shannon + */ + +public class Response { + protected int index; // internal index (updated during the parse) + protected int pindex; // index after parse, for reset + protected int size; // number of valid bytes in our buffer + protected byte[] buffer = null; + protected int type = 0; + protected String tag = null; + /** @since JavaMail 1.5.4 */ + protected Exception ex; + protected boolean utf8; + + private static final int increment = 100; + + // The first and second bits indicate whether this response + // is a Continuation, Tagged or Untagged + public final static int TAG_MASK = 0x03; + public final static int CONTINUATION = 0x01; + public final static int TAGGED = 0x02; + public final static int UNTAGGED = 0x03; + + // The third, fourth and fifth bits indicate whether this response + // is an OK, NO, BAD or BYE response + public final static int TYPE_MASK = 0x1C; + public final static int OK = 0x04; + public final static int NO = 0x08; + public final static int BAD = 0x0C; + public final static int BYE = 0x10; + + // The sixth bit indicates whether a BYE response is synthetic or real + public final static int SYNTHETIC = 0x20; + + /** + * An ATOM is any CHAR delimited by: + * SPACE | CTL | '(' | ')' | '{' | '%' | '*' | '"' | '\' | ']' + * (CTL is handled in readDelimString.) + */ + private static String ATOM_CHAR_DELIM = " (){%*\"\\]"; + + /** + * An ASTRING_CHAR is any CHAR delimited by: + * SPACE | CTL | '(' | ')' | '{' | '%' | '*' | '"' | '\' + * (CTL is handled in readDelimString.) + */ + private static String ASTRING_CHAR_DELIM = " (){%*\"\\"; + + public Response(String s) { + this(s, true); + } + + /** + * Constructor for testing. + * + * @param s the response string + * @param supportsUtf8 allow UTF-8 in response? + * @since JavaMail 1.6.0 + */ + public Response(String s, boolean supportsUtf8) { + if (supportsUtf8) + buffer = s.getBytes(StandardCharsets.UTF_8); + else + buffer = s.getBytes(StandardCharsets.US_ASCII); + size = buffer.length; + utf8 = supportsUtf8; + parse(); + } + + /** + * Read a new Response from the given Protocol + * + * @param p the Protocol object + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public Response(Protocol p) throws IOException, ProtocolException { + // read one response into 'buffer' + ByteArray ba = p.getResponseBuffer(); + ByteArray response = p.getInputStream().readResponse(ba); + buffer = response.getBytes(); + size = response.getCount() - 2; // Skip the terminating CRLF + utf8 = p.supportsUtf8(); + + parse(); + } + + /** + * Copy constructor. + * + * @param r the Response to copy + */ + public Response(Response r) { + index = r.index; + pindex = r.pindex; + size = r.size; + buffer = r.buffer; + type = r.type; + tag = r.tag; + ex = r.ex; + utf8 = r.utf8; + } + + /** + * Return a Response object that looks like a BYE protocol response. + * Include the details of the exception in the response string. + * + * @param ex the exception + * @return the synthetic Response object + */ + public static Response byeResponse(Exception ex) { + String err = "* BYE Jakarta Mail Exception: " + ex.toString(); + err = err.replace('\r', ' ').replace('\n', ' '); + Response r = new Response(err); + r.type |= SYNTHETIC; + r.ex = ex; + return r; + } + + /** + * Does the server support UTF-8? + * + * @return true if the server supports UTF-8 + * @since JavaMail 1.6.0 + */ + public boolean supportsUtf8() { + return utf8; + } + + private void parse() { + index = 0; // position internal index at start + + if (size == 0) // empty line + return; + if (buffer[index] == '+') { // Continuation statement + type |= CONTINUATION; + index += 1; // Position beyond the '+' + return; // return + } else if (buffer[index] == '*') { // Untagged statement + type |= UNTAGGED; + index += 1; // Position beyond the '*' + } else { // Tagged statement + type |= TAGGED; + tag = readAtom(); // read the TAG, index positioned beyond tag + if (tag == null) + tag = ""; // avoid possible NPE + } + + int mark = index; // mark + String s = readAtom(); // updates index + if (s == null) + s = ""; // avoid possible NPE + if (s.equalsIgnoreCase("OK")) + type |= OK; + else if (s.equalsIgnoreCase("NO")) + type |= NO; + else if (s.equalsIgnoreCase("BAD")) + type |= BAD; + else if (s.equalsIgnoreCase("BYE")) + type |= BYE; + else + index = mark; // reset + + pindex = index; + return; + } + + public void skipSpaces() { + while (index < size && buffer[index] == ' ') + index++; + } + + /** + * Skip past any spaces. If the next non-space character is c, + * consume it and return true. Otherwise stop at that point + * and return false. + * + * @param c the character to look for + * @return true if the character is found + */ + public boolean isNextNonSpace(char c) { + skipSpaces(); + if (index < size && buffer[index] == (byte)c) { + index++; + return true; + } + return false; + } + + /** + * Skip to the next space, for use in error recovery while parsing. + */ + public void skipToken() { + while (index < size && buffer[index] != ' ') + index++; + } + + public void skip(int count) { + index += count; + } + + public byte peekByte() { + if (index < size) + return buffer[index]; + else + return 0; // XXX - how else to signal error? + } + + /** + * Return the next byte from this Statement. + * + * @return the next byte + */ + public byte readByte() { + if (index < size) + return buffer[index++]; + else + return 0; // XXX - how else to signal error? + } + + /** + * Extract an ATOM, starting at the current position. Updates + * the internal index to beyond the Atom. + * + * @return an Atom + */ + public String readAtom() { + return readDelimString(ATOM_CHAR_DELIM); + } + + /** + * Extract a string stopping at control characters or any + * character in delim. + */ + private String readDelimString(String delim) { + skipSpaces(); + + if (index >= size) // already at end of response + return null; + + int b; + int start = index; + while (index < size && ((b = (((int)buffer[index])&0xff)) >= ' ') && + delim.indexOf((char)b) < 0 && b != 0x7f) + index++; + + return toString(buffer, start, index); + } + + /** + * Read a string as an arbitrary sequence of characters, + * stopping at the delimiter Used to read part of a + * response code inside []. + * + * @param delim the delimiter character + * @return the string + */ + public String readString(char delim) { + skipSpaces(); + + if (index >= size) // already at end of response + return null; + + int start = index; + while (index < size && buffer[index] != delim) + index++; + + return toString(buffer, start, index); + } + + public String[] readStringList() { + return readStringList(false); + } + + public String[] readAtomStringList() { + return readStringList(true); + } + + private String[] readStringList(boolean atom) { + skipSpaces(); + + if (buffer[index] != '(') { // not what we expected + return null; + } + index++; // skip '(' + + // to handle buggy IMAP servers, we tolerate multiple spaces as + // well as spaces after the left paren or before the right paren + List result = new ArrayList<>(); + while (!isNextNonSpace(')')) { + String s = atom ? readAtomString() : readString(); + if (s == null) // not the expected string or atom + break; + result.add(s); + } + + return result.toArray(new String[result.size()]); + } + + /** + * Extract an integer, starting at the current position. Updates the + * internal index to beyond the number. Returns -1 if a number was + * not found. + * + * @return a number + */ + public int readNumber() { + // Skip leading spaces + skipSpaces(); + + int start = index; + while (index < size && Character.isDigit((char)buffer[index])) + index++; + + if (index > start) { + try { + return ASCIIUtility.parseInt(buffer, start, index); + } catch (NumberFormatException nex) { } + } + + return -1; + } + + /** + * Extract a long number, starting at the current position. Updates the + * internal index to beyond the number. Returns -1 if a long number + * was not found. + * + * @return a long + */ + public long readLong() { + // Skip leading spaces + skipSpaces(); + + int start = index; + while (index < size && Character.isDigit((char)buffer[index])) + index++; + + if (index > start) { + try { + return ASCIIUtility.parseLong(buffer, start, index); + } catch (NumberFormatException nex) { } + } + + return -1; + } + + /** + * Extract a NSTRING, starting at the current position. Return it as + * a String. The sequence 'NIL' is returned as null + * + * NSTRING := QuotedString | Literal | "NIL" + * + * @return a String + */ + public String readString() { + return (String)parseString(false, true); + } + + /** + * Extract a NSTRING, starting at the current position. Return it as + * a ByteArrayInputStream. The sequence 'NIL' is returned as null + * + * NSTRING := QuotedString | Literal | "NIL" + * + * @return a ByteArrayInputStream + */ + public ByteArrayInputStream readBytes() { + ByteArray ba = readByteArray(); + if (ba != null) + return ba.toByteArrayInputStream(); + else + return null; + } + + /** + * Extract a NSTRING, starting at the current position. Return it as + * a ByteArray. The sequence 'NIL' is returned as null + * + * NSTRING := QuotedString | Literal | "NIL" + * + * @return a ByteArray + */ + public ByteArray readByteArray() { + /* + * Special case, return the data after the continuation uninterpreted. + * It's usually a challenge for an AUTHENTICATE command. + */ + if (isContinuation()) { + skipSpaces(); + return new ByteArray(buffer, index, size - index); + } + return (ByteArray)parseString(false, false); + } + + /** + * Extract an ASTRING, starting at the current position + * and return as a String. An ASTRING can be a QuotedString, a + * Literal or an Atom (plus ']'). + * + * Any errors in parsing returns null + * + * ASTRING := QuotedString | Literal | 1*ASTRING_CHAR + * + * @return a String + */ + public String readAtomString() { + return (String)parseString(true, true); + } + + /** + * Generic parsing routine that can parse out a Quoted-String, + * Literal or Atom and return the parsed token as a String + * or a ByteArray. Errors or NIL data will return null. + */ + private Object parseString(boolean parseAtoms, boolean returnString) { + byte b; + + // Skip leading spaces + skipSpaces(); + + b = buffer[index]; + if (b == '"') { // QuotedString + index++; // skip the quote + int start = index; + int copyto = index; + + while (index < size && (b = buffer[index]) != '"') { + if (b == '\\') // skip escaped byte + index++; + if (index != copyto) { // only copy if we need to + // Beware: this is a destructive copy. I'm + // pretty sure this is OK, but ... ;> + buffer[copyto] = buffer[index]; + } + copyto++; + index++; + } + if (index >= size) { + // didn't find terminating quote, something is seriously wrong + //throw new ArrayIndexOutOfBoundsException( + // "index = " + index + ", size = " + size); + return null; + } else + index++; // skip past the terminating quote + + if (returnString) + return toString(buffer, start, copyto); + else + return new ByteArray(buffer, start, copyto-start); + } else if (b == '{') { // Literal + int start = ++index; // note the start position + + while (buffer[index] != '}') + index++; + + int count = 0; + try { + count = ASCIIUtility.parseInt(buffer, start, index); + } catch (NumberFormatException nex) { + // throw new ParsingException(); + return null; + } + + start = index + 3; // skip "}\r\n" + index = start + count; // position index to beyond the literal + + if (returnString) // return as String + return toString(buffer, start, start + count); + else + return new ByteArray(buffer, start, count); + } else if (parseAtoms) { // parse as ASTRING-CHARs + int start = index; // track this, so that we can use to + // creating ByteArrayInputStream below. + String s = readDelimString(ASTRING_CHAR_DELIM); + if (returnString) + return s; + else // *very* unlikely + return new ByteArray(buffer, start, index); + } else if (b == 'N' || b == 'n') { // the only valid value is 'NIL' + index += 3; // skip past NIL + return null; + } + return null; // Error + } + + private String toString(byte[] buffer, int start, int end) { + return utf8 ? + new String(buffer, start, end - start, StandardCharsets.UTF_8) : + ASCIIUtility.toString(buffer, start, end); + } + + public int getType() { + return type; + } + + public boolean isContinuation() { + return ((type & TAG_MASK) == CONTINUATION); + } + + public boolean isTagged() { + return ((type & TAG_MASK) == TAGGED); + } + + public boolean isUnTagged() { + return ((type & TAG_MASK) == UNTAGGED); + } + + public boolean isOK() { + return ((type & TYPE_MASK) == OK); + } + + public boolean isNO() { + return ((type & TYPE_MASK) == NO); + } + + public boolean isBAD() { + return ((type & TYPE_MASK) == BAD); + } + + public boolean isBYE() { + return ((type & TYPE_MASK) == BYE); + } + + public boolean isSynthetic() { + return ((type & SYNTHETIC) == SYNTHETIC); + } + + /** + * Return the tag, if this is a tagged statement. + * + * @return tag of this tagged statement + */ + public String getTag() { + return tag; + } + + /** + * Return the rest of the response as a string, usually used to + * return the arbitrary message text after a NO response. + * + * @return the rest of the response + */ + public String getRest() { + skipSpaces(); + return toString(buffer, index, size); + } + + /** + * Return the exception for a synthetic BYE response. + * + * @return the exception + * @since JavaMail 1.5.4 + */ + public Exception getException() { + return ex; + } + + /** + * Reset pointer to beginning of response. + */ + public void reset() { + index = pindex; + } + + @Override + public String toString() { + return toString(buffer, 0, size); + } + +} diff --git a/app/src/main/java/com/sun/mail/iap/ResponseHandler.java b/app/src/main/java/com/sun/mail/iap/ResponseHandler.java new file mode 100644 index 0000000000..7d5a61c822 --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ResponseHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +/** + * This class + * + * @author John Mani + */ + +public interface ResponseHandler { + public void handleResponse(Response r); +} diff --git a/app/src/main/java/com/sun/mail/iap/ResponseInputStream.java b/app/src/main/java/com/sun/mail/iap/ResponseInputStream.java new file mode 100644 index 0000000000..9e05b18f1f --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/ResponseInputStream.java @@ -0,0 +1,158 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.iap; + +import java.io.*; +import com.sun.mail.iap.ByteArray; +import com.sun.mail.util.ASCIIUtility; + +/** + * + * Inputstream that is used to read a Response. + * + * @author Arun Krishnan + * @author Bill Shannon + */ + +public class ResponseInputStream { + + private static final int minIncrement = 256; + private static final int maxIncrement = 256 * 1024; + private static final int incrementSlop = 16; + + // where we read from + private BufferedInputStream bin; + + /** + * Constructor. + * + * @param in the InputStream to wrap + */ + public ResponseInputStream(InputStream in) { + bin = new BufferedInputStream(in, 2 * 1024); + } + + /** + * Read a Response from the InputStream. + * + * @return ByteArray that contains the Response + * @exception IOException for I/O errors + */ + public ByteArray readResponse() throws IOException { + return readResponse(null); + } + + /** + * Read a Response from the InputStream. + * + * @param ba the ByteArray in which to store the response, or null + * @return ByteArray that contains the Response + * @exception IOException for I/O errors + */ + public ByteArray readResponse(ByteArray ba) throws IOException { + if (ba == null) + ba = new ByteArray(new byte[128], 0, 128); + + byte[] buffer = ba.getBytes(); + int idx = 0; + for (;;) { // read until CRLF with no preceeding literal + // XXX - b needs to be an int, to handle bytes with value 0xff + int b = 0; + boolean gotCRLF=false; + + // Read a CRLF terminated line from the InputStream + while (!gotCRLF && + ((b = bin.read()) != -1)) { + if (b == '\n') { + if ((idx > 0) && buffer[idx-1] == '\r') + gotCRLF = true; + } + if (idx >= buffer.length) { + int incr = buffer.length; + if (incr > maxIncrement) + incr = maxIncrement; + ba.grow(incr); + buffer = ba.getBytes(); + } + buffer[idx++] = (byte)b; + } + + if (b == -1) + throw new IOException("Connection dropped by server?"); + + // Now lets check for literals : {}CRLF + // Note: index needs to >= 5 for the above sequence to occur + if (idx < 5 || buffer[idx-3] != '}') + break; + + int i; + // look for left curly + for (i = idx - 4; i >= 0; i--) + if (buffer[i] == '{') + break; + + if (i < 0) // Nope, not a literal ? + break; + + int count = 0; + // OK, handle the literal .. + try { + count = ASCIIUtility.parseInt(buffer, i+1, idx-3); + } catch (NumberFormatException e) { + break; + } + + // Now read 'count' bytes. (Note: count could be 0) + if (count > 0) { + int avail = buffer.length - idx; // available space in buffer + if (count + incrementSlop > avail) { + // need count-avail more bytes + ba.grow(minIncrement > count + incrementSlop - avail ? + minIncrement : count + incrementSlop - avail); + buffer = ba.getBytes(); + } + + /* + * read() might not return all the bytes in one shot, + * so call repeatedly till we are done + */ + int actual; + while (count > 0) { + actual = bin.read(buffer, idx, count); + if (actual == -1) + throw new IOException("Connection dropped by server?"); + count -= actual; + idx += actual; + } + } + // back to top of loop to read until CRLF + } + ba.setCount(idx); + return ba; + } + + /** + * How much buffered data do we have? + * + * @return number of bytes available + * @exception IOException if the stream has been closed + * @since JavaMail 1.5.4 + */ + public int available() throws IOException { + return bin.available(); + } +} diff --git a/app/src/main/java/com/sun/mail/iap/package.html b/app/src/main/java/com/sun/mail/iap/package.html new file mode 100644 index 0000000000..2607e33a2e --- /dev/null +++ b/app/src/main/java/com/sun/mail/iap/package.html @@ -0,0 +1,33 @@ + + + + + + +com.sun.mail.iap package + + + +

+This package includes internal IMAP support classes and +SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. +

+ + + diff --git a/app/src/main/java/com/sun/mail/imap/ACL.java b/app/src/main/java/com/sun/mail/imap/ACL.java new file mode 100644 index 0000000000..ee99c95e2c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/ACL.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.*; + +/** + * An access control list entry for a particular authentication identifier + * (user or group). Associates a set of Rights with the identifier. + * See RFC 2086. + *

+ * + * @author Bill Shannon + */ + +public class ACL implements Cloneable { + + private String name; + private Rights rights; + + /** + * Construct an ACL entry for the given identifier and with no rights. + * + * @param name the identifier name + */ + public ACL(String name) { + this.name = name; + this.rights = new Rights(); + } + + /** + * Construct an ACL entry for the given identifier with the given rights. + * + * @param name the identifier name + * @param rights the rights + */ + public ACL(String name, Rights rights) { + this.name = name; + this.rights = rights; + } + + /** + * Get the identifier name for this ACL entry. + * + * @return the identifier name + */ + public String getName() { + return name; + } + + /** + * Set the rights associated with this ACL entry. + * + * @param rights the rights + */ + public void setRights(Rights rights) { + this.rights = rights; + } + + /** + * Get the rights associated with this ACL entry. + * Returns the actual Rights object referenced by this ACL; + * modifications to the Rights object will effect this ACL. + * + * @return the rights + */ + public Rights getRights() { + return rights; + } + + /** + * Clone this ACL entry. + */ + @Override + public Object clone() throws CloneNotSupportedException { + ACL acl = (ACL)super.clone(); + acl.rights = (Rights)this.rights.clone(); + return acl; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/AppendUID.java b/app/src/main/java/com/sun/mail/imap/AppendUID.java new file mode 100644 index 0000000000..56c77fed82 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/AppendUID.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import com.sun.mail.iap.*; + +/** + * Information from the APPENDUID response code + * defined by the UIDPLUS extension - + * RFC 4315. + * + * @author Bill Shannon + */ + +public class AppendUID { + public long uidvalidity = -1; + public long uid = -1; + + public AppendUID(long uidvalidity, long uid) { + this.uidvalidity = uidvalidity; + this.uid = uid; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/CopyUID.java b/app/src/main/java/com/sun/mail/imap/CopyUID.java new file mode 100644 index 0000000000..3648fc5cb2 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/CopyUID.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import com.sun.mail.imap.protocol.UIDSet; + +/** + * Information from the COPYUID response code + * defined by the UIDPLUS extension - + * RFC 4315. + * + * @author Bill Shannon + */ + +public class CopyUID { + public long uidvalidity = -1; + public UIDSet[] src; + public UIDSet[] dst; + + public CopyUID(long uidvalidity, UIDSet[] src, UIDSet[] dst) { + this.uidvalidity = uidvalidity; + this.src = src; + this.dst = dst; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/DefaultFolder.java b/app/src/main/java/com/sun/mail/imap/DefaultFolder.java new file mode 100644 index 0000000000..9dab04fb5a --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/DefaultFolder.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.MethodNotSupportedException; +import com.sun.mail.iap.ProtocolException; +import com.sun.mail.imap.protocol.IMAPProtocol; +import com.sun.mail.imap.protocol.ListInfo; + +/** + * The default IMAP folder (root of the naming hierarchy). + * + * @author John Mani + */ + +public class DefaultFolder extends IMAPFolder { + + protected DefaultFolder(IMAPStore store) { + super("", UNKNOWN_SEPARATOR, store, null); + exists = true; // of course + type = HOLDS_FOLDERS; // obviously + } + + @Override + public synchronized String getName() { + return fullName; + } + + @Override + public Folder getParent() { + return null; + } + + @Override + public synchronized Folder[] list(final String pattern) + throws MessagingException { + ListInfo[] li = null; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.list("", pattern); + } + }); + + if (li == null) + return new Folder[0]; + + IMAPFolder[] folders = new IMAPFolder[li.length]; + for (int i = 0; i < folders.length; i++) + folders[i] = ((IMAPStore)store).newIMAPFolder(li[i]); + return folders; + } + + @Override + public synchronized Folder[] listSubscribed(final String pattern) + throws MessagingException { + ListInfo[] li = null; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.lsub("", pattern); + } + }); + + if (li == null) + return new Folder[0]; + + IMAPFolder[] folders = new IMAPFolder[li.length]; + for (int i = 0; i < folders.length; i++) + folders[i] = ((IMAPStore)store).newIMAPFolder(li[i]); + return folders; + } + + @Override + public boolean hasNewMessages() throws MessagingException { + // Not applicable on DefaultFolder + return false; + } + + @Override + public Folder getFolder(String name) throws MessagingException { + return ((IMAPStore)store).newIMAPFolder(name, UNKNOWN_SEPARATOR); + } + + @Override + public boolean delete(boolean recurse) throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot delete Default Folder"); + } + + @Override + public boolean renameTo(Folder f) throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot rename Default Folder"); + } + + @Override + public void appendMessages(Message[] msgs) throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot append to Default Folder"); + } + + @Override + public Message[] expunge() throws MessagingException { + // Not applicable on DefaultFolder + throw new MethodNotSupportedException("Cannot expunge Default Folder"); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPBodyPart.java b/app/src/main/java/com/sun/mail/imap/IMAPBodyPart.java new file mode 100644 index 0000000000..181bb94512 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPBodyPart.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; + +import java.util.Enumeration; +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.ReadableMime; +import com.sun.mail.util.LineOutputStream; +import com.sun.mail.util.SharedByteArrayOutputStream; +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; + +/** + * An IMAP body part. + * + * @author John Mani + * @author Bill Shannon + */ + +public class IMAPBodyPart extends MimeBodyPart implements ReadableMime { + private IMAPMessage message; + private BODYSTRUCTURE bs; + private String sectionId; + + // processed values .. + private String type; + private String description; + + private boolean headersLoaded = false; + + private static final boolean decodeFileName = + PropUtil.getBooleanSystemProperty("mail.mime.decodefilename", false); + + protected IMAPBodyPart(BODYSTRUCTURE bs, String sid, IMAPMessage message) { + super(); + this.bs = bs; + this.sectionId = sid; + this.message = message; + // generate content-type + ContentType ct = new ContentType(bs.type, bs.subtype, bs.cParams); + type = ct.toString(); + } + + /* Override this method to make it a no-op, rather than throw + * an IllegalWriteException. This will permit IMAPBodyParts to + * be inserted in newly crafted MimeMessages, especially when + * forwarding or replying to messages. + */ + @Override + protected void updateHeaders() { + return; + } + + @Override + public int getSize() throws MessagingException { + return bs.size; + } + + @Override + public int getLineCount() throws MessagingException { + return bs.lines; + } + + @Override + public String getContentType() throws MessagingException { + return type; + } + + @Override + public String getDisposition() throws MessagingException { + return bs.disposition; + } + + @Override + public void setDisposition(String disposition) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String getEncoding() throws MessagingException { + return bs.encoding; + } + + @Override + public String getContentID() throws MessagingException { + return bs.id; + } + + @Override + public String getContentMD5() throws MessagingException { + return bs.md5; + } + + @Override + public void setContentMD5(String md5) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String getDescription() throws MessagingException { + if (description != null) // cached value ? + return description; + + if (bs.description == null) + return null; + + try { + description = MimeUtility.decodeText(bs.description); + } catch (UnsupportedEncodingException ex) { + description = bs.description; + } + + return description; + } + + @Override + public void setDescription(String description, String charset) + throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String getFileName() throws MessagingException { + String filename = null; + if (bs.dParams != null) + filename = bs.dParams.get("filename"); + if ((filename == null || filename.isEmpty()) && bs.cParams != null) + filename = bs.cParams.get("name"); + if (decodeFileName && filename != null) { + try { + filename = MimeUtility.decodeText(filename); + } catch (UnsupportedEncodingException ex) { + throw new MessagingException("Can't decode filename", ex); + } + } + return filename; + } + + @Override + public void setFileName(String filename) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + protected InputStream getContentStream() throws MessagingException { + InputStream is = null; + boolean pk = message.getPeek(); // acquire outside of message cache lock + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(message.getMessageCacheLock()) { + try { + IMAPProtocol p = message.getProtocol(); + + // Check whether this message is expunged + message.checkExpunged(); + + if (p.isREV1() && (message.getFetchBlockSize() != -1)) + return new IMAPInputStream(message, sectionId, + message.ignoreBodyStructureSize() ? -1 : bs.size, pk); + + // Else, vanila IMAP4, no partial fetch + + int seqnum = message.getSequenceNumber(); + BODY b; + if (pk) + b = p.peekBody(seqnum, sectionId); + else + b = p.fetchBody(seqnum, sectionId); + if (b != null) + is = b.getByteArrayInputStream(); + } catch (ConnectionException cex) { + throw new FolderClosedException( + message.getFolder(), cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + if (is == null) { + message.forceCheckExpunged(); // may throw MessageRemovedException + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + is = new ByteArrayInputStream(new byte[0]); + } + return is; + } + + /** + * Return the MIME format stream of headers for this body part. + */ + private InputStream getHeaderStream() throws MessagingException { + if (!message.isREV1()) + loadHeaders(); // will be needed below + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(message.getMessageCacheLock()) { + try { + IMAPProtocol p = message.getProtocol(); + + // Check whether this message got expunged + message.checkExpunged(); + + if (p.isREV1()) { + int seqnum = message.getSequenceNumber(); + BODY b = p.peekBody(seqnum, sectionId + ".MIME"); + + if (b == null) + throw new MessagingException("Failed to fetch headers"); + + ByteArrayInputStream bis = b.getByteArrayInputStream(); + if (bis == null) + throw new MessagingException("Failed to fetch headers"); + return bis; + + } else { + // Can't read it from server, have to fake it + SharedByteArrayOutputStream bos = + new SharedByteArrayOutputStream(0); + LineOutputStream los = new LineOutputStream(bos); + + try { + // Write out the header + Enumeration hdrLines + = super.getAllHeaderLines(); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + } catch (IOException ioex) { + // should never happen + } finally { + try { + los.close(); + } catch (IOException cex) { } + } + return bos.toStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException( + message.getFolder(), cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Return the MIME format stream corresponding to this message part. + * + * @return the MIME format stream + * @since JavaMail 1.4.5 + */ + @Override + public InputStream getMimeStream() throws MessagingException { + /* + * The IMAP protocol doesn't support returning the entire + * part content in one operation so we have to fake it by + * concatenating the header stream and the content stream. + */ + return new SequenceInputStream(getHeaderStream(), getContentStream()); + } + + @Override + public synchronized DataHandler getDataHandler() + throws MessagingException { + if (dh == null) { + if (bs.isMulti()) + dh = new DataHandler( + new IMAPMultipartDataSource( + this, bs.bodies, sectionId, message) + ); + else if (bs.isNested() && message.isREV1() && bs.envelope != null) + dh = new DataHandler( + new IMAPNestedMessage(message, + bs.bodies[0], + bs.envelope, + sectionId), + type + ); + } + + return super.getDataHandler(); + } + + @Override + public void setDataHandler(DataHandler content) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void setContent(Object o, String type) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void setContent(Multipart mp) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public String[] getHeader(String name) throws MessagingException { + loadHeaders(); + return super.getHeader(name); + } + + @Override + public void setHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void addHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public void removeHeader(String name) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public Enumeration

getAllHeaders() throws MessagingException { + loadHeaders(); + return super.getAllHeaders(); + } + + @Override + public Enumeration
getMatchingHeaders(String[] names) + throws MessagingException { + loadHeaders(); + return super.getMatchingHeaders(names); + } + + @Override + public Enumeration
getNonMatchingHeaders(String[] names) + throws MessagingException { + loadHeaders(); + return super.getNonMatchingHeaders(names); + } + + @Override + public void addHeaderLine(String line) throws MessagingException { + throw new IllegalWriteException("IMAPBodyPart is read-only"); + } + + @Override + public Enumeration getAllHeaderLines() throws MessagingException { + loadHeaders(); + return super.getAllHeaderLines(); + } + + @Override + public Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException { + loadHeaders(); + return super.getMatchingHeaderLines(names); + } + + @Override + public Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException { + loadHeaders(); + return super.getNonMatchingHeaderLines(names); + } + + private synchronized void loadHeaders() throws MessagingException { + if (headersLoaded) + return; + + // "headers" should never be null since it's set in the constructor. + // If something did go wrong this will fix it, but is an unsynchronized + // assignment of "headers". + if (headers == null) + headers = new InternetHeaders(); + + // load headers + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(message.getMessageCacheLock()) { + try { + IMAPProtocol p = message.getProtocol(); + + // Check whether this message got expunged + message.checkExpunged(); + + if (p.isREV1()) { + int seqnum = message.getSequenceNumber(); + BODY b = p.peekBody(seqnum, sectionId + ".MIME"); + + if (b == null) + throw new MessagingException("Failed to fetch headers"); + + ByteArrayInputStream bis = b.getByteArrayInputStream(); + if (bis == null) + throw new MessagingException("Failed to fetch headers"); + + headers.load(bis); + + } else { + + // RFC 1730 does not provide for fetching BodyPart headers + // So, just dump the RFC1730 BODYSTRUCTURE into the + // headerStore + + // Content-Type + headers.addHeader("Content-Type", type); + // Content-Transfer-Encoding + headers.addHeader("Content-Transfer-Encoding", bs.encoding); + // Content-Description + if (bs.description != null) + headers.addHeader("Content-Description", + bs.description); + // Content-ID + if (bs.id != null) + headers.addHeader("Content-ID", bs.id); + // Content-MD5 + if (bs.md5 != null) + headers.addHeader("Content-MD5", bs.md5); + } + } catch (ConnectionException cex) { + throw new FolderClosedException( + message.getFolder(), cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + headersLoaded = true; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPFolder.java b/app/src/main/java/com/sun/mail/imap/IMAPFolder.java new file mode 100644 index 0000000000..74f2f56b96 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPFolder.java @@ -0,0 +1,4153 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import java.util.Vector; +import java.util.Hashtable; +import java.util.List; +import java.util.ArrayList; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Locale; +import java.util.logging.Level; +import java.io.*; +import java.net.SocketTimeoutException; +import java.nio.channels.SocketChannel; + +import javax.mail.*; +import javax.mail.event.*; +import javax.mail.internet.*; +import javax.mail.search.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.CRLFOutputStream; +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; + +/** + * This class implements an IMAP folder.

+ * + * A closed IMAPFolder object shares a protocol connection with its IMAPStore + * object. When the folder is opened, it gets its own protocol connection.

+ * + * Applications that need to make use of IMAP-specific features may cast + * a Folder object to an IMAPFolder object and + * use the methods on this class.

+ * + * The {@link #getQuota getQuota} and + * {@link #setQuota setQuota} methods support the IMAP QUOTA extension. + * Refer to RFC 2087 + * for more information.

+ * + * The {@link #getACL getACL}, {@link #addACL addACL}, + * {@link #removeACL removeACL}, {@link #addRights addRights}, + * {@link #removeRights removeRights}, {@link #listRights listRights}, and + * {@link #myRights myRights} methods support the IMAP ACL extension. + * Refer to RFC 2086 + * for more information.

+ * + * The {@link #getSortedMessages getSortedMessages} + * methods support the IMAP SORT extension. + * Refer to RFC 5256 + * for more information.

+ * + * The {@link #open(int,com.sun.mail.imap.ResyncData) open(int,ResyncData)} + * method and {@link com.sun.mail.imap.ResyncData ResyncData} class supports + * the IMAP CONDSTORE and QRESYNC extensions. + * Refer to RFC 4551 + * and RFC 5162 + * for more information.

+ * + * The {@link #doCommand doCommand} method and + * {@link IMAPFolder.ProtocolCommand IMAPFolder.ProtocolCommand} + * interface support use of arbitrary IMAP protocol commands.

+ * + * See the com.sun.mail.imap package + * documentation for further information on the IMAP protocol provider.

+ * + * WARNING: The APIs unique to this class should be + * considered EXPERIMENTAL. They may be changed in the + * future in ways that are incompatible with applications using the + * current APIs. + * + * @author John Mani + * @author Bill Shannon + * @author Jim Glennon + */ + +/* + * The folder object itself serves as a lock for the folder's state + * EXCEPT for the message cache (see below), typically by using + * synchronized methods. When checking that a folder is open or + * closed, the folder's lock must be held. It's important that the + * folder's lock is acquired before the messageCacheLock (see below). + * Thus, the locking hierarchy is that the folder lock, while optional, + * must be acquired before the messageCacheLock, if it's acquired at + * all. Be especially careful of callbacks that occur while holding + * the messageCacheLock into (e.g.) superclass Folder methods that are + * synchronized. Note that methods in IMAPMessage will acquire the + * messageCacheLock without acquiring the folder lock.

+ * + * When a folder is opened, it creates a messageCache (a Vector) of + * empty IMAPMessage objects. Each Message has a messageNumber - which + * is its index into the messageCache, and a sequenceNumber - which is + * its IMAP sequence-number. All operations on a Message which involve + * communication with the server, use the message's sequenceNumber.

+ * + * The most important thing to note here is that the server can send + * unsolicited EXPUNGE notifications as part of the responses for "most" + * commands. Refer RFC 3501, sections 5.3 & 5.5 for gory details. Also, + * the server sends these notifications AFTER the message has been + * expunged. And once a message is expunged, the sequence-numbers of + * those messages after the expunged one are renumbered. This essentially + * means that the mapping between *any* Message and its sequence-number + * can change in the period when a IMAP command is issued and its responses + * are processed. Hence we impose a strict locking model as follows:

+ * + * We define one mutex per folder - this is just a Java Object (named + * messageCacheLock). Any time a command is to be issued to the IMAP + * server (i.e., anytime the corresponding IMAPProtocol method is + * invoked), follow the below style: + * + * synchronized (messageCacheLock) { // ACQUIRE LOCK + * issue command () + * + * // The response processing is typically done within + * // the handleResponse() callback. A few commands (Fetch, + * // Expunge) return *all* responses and hence their + * // processing is done here itself. Now, as part of the + * // processing unsolicited EXPUNGE responses, we renumber + * // the necessary sequence-numbers. Thus the renumbering + * // happens within this critical-region, surrounded by + * // locks. + * process responses () + * } // RELEASE LOCK + * + * This technique is used both by methods in IMAPFolder and by methods + * in IMAPMessage and other classes that operate on data in the folder. + * Note that holding the messageCacheLock has the side effect of + * preventing the folder from being closed, and thus ensuring that the + * folder's protocol object is still valid. The protocol object should + * only be accessed while holding the messageCacheLock (except for calls + * to IMAPProtocol.isREV1(), which don't need to be protected because it + * doesn't access the server). + * + * Note that interactions with the Store's protocol connection do + * not have to be protected as above, since the Store's protocol is + * never in a "meaningful" SELECT-ed state. + */ + +public class IMAPFolder extends Folder implements UIDFolder, ResponseHandler { + + protected volatile String fullName; // full name + protected String name; // name + protected int type; // folder type. + protected char separator; // separator + protected Flags availableFlags; // available flags + protected Flags permanentFlags; // permanent flags + protected volatile boolean exists; // whether this folder really exists ? + protected boolean isNamespace = false; // folder is a namespace name + protected volatile String[] attributes;// name attributes from LIST response + + protected volatile IMAPProtocol protocol; // this folder's protocol object + protected MessageCache messageCache;// message cache + // accessor lock for message cache + protected final Object messageCacheLock = new Object(); + + protected Hashtable uidTable; // UID->Message hashtable + + /* An IMAP delimiter is a 7bit US-ASCII character. (except NUL). + * We use '\uffff' (a non 7bit character) to indicate that we havent + * yet determined what the separator character is. + * We use '\u0000' (NUL) to indicate that no separator character + * exists, i.e., a flat hierarchy + */ + static final protected char UNKNOWN_SEPARATOR = '\uffff'; + + private volatile boolean opened = false; // is this folder opened ? + + /* This field tracks the state of this folder. If the folder is closed + * due to external causes (i.e, not thru the close() method), then + * this field will remain false. If the folder is closed thru the + * close() method, then this field is set to true. + * + * If reallyClosed is false, then a FolderClosedException is + * generated when a method is invoked on any Messaging object + * owned by this folder. If reallyClosed is true, then the + * IllegalStateException runtime exception is thrown. + */ + private boolean reallyClosed = true; + + /* + * The idleState field supports the IDLE command. + * Normally when executing an IMAP command we hold the + * messageCacheLock and often the folder lock (see above). + * While executing the IDLE command we can't hold either + * of these locks or it would prevent other threads from + * entering Folder methods even far enough to check whether + * an IDLE command is in progress. We need to check before + * issuing another command so that we can abort the IDLE + * command. + * + * The idleState field is protected by the messageCacheLock. + * The RUNNING state is the normal state and means no IDLE + * command is in progress. The IDLE state means we've issued + * an IDLE command and are reading responses. The ABORTING + * state means we've sent the DONE continuation command and + * are waiting for the thread running the IDLE command to + * break out of its read loop. + * + * When an IDLE command is in progress, the thread calling + * the idle method will be reading from the IMAP connection + * while holding neither the folder lock nor the messageCacheLock. + * It's obviously critical that no other thread try to send a + * command or read from the connection while in this state. + * However, other threads can send the DONE continuation + * command that will cause the server to break out of the IDLE + * loop and send the ending tag response to the IDLE command. + * The thread in the idle method that's reading the responses + * from the IDLE command will see this ending response and + * complete the idle method, setting the idleState field back + * to RUNNING, and notifying any threads waiting to use the + * connection. + * + * All uses of the IMAP connection (IMAPProtocol object) must + * be done while holding the messageCacheLock and must be + * preceeded by a check to make sure an IDLE command is not + * running, and abort the IDLE command if necessary. While + * waiting for the IDLE command to complete, these other threads + * will give up the messageCacheLock, but might still be holding + * the folder lock. This check is done by the getProtocol() + * method, resulting in a typical usage pattern of: + * + * synchronized (messageCacheLock) { + * IMAPProtocol p = getProtocol(); // may block waiting for IDLE + * // ... use protocol + * } + */ + private static final int RUNNING = 0; // not doing IDLE command + private static final int IDLE = 1; // IDLE command in effect + private static final int ABORTING = 2; // IDLE command aborting + private int idleState = RUNNING; + private IdleManager idleManager; + + private volatile int total = -1; // total number of messages in the + // message cache + private volatile int recent = -1; // number of recent messages + private int realTotal = -1; // total number of messages on + // the server + private long uidvalidity = -1; // UIDValidity + private long uidnext = -1; // UIDNext + private boolean uidNotSticky = false; // RFC 4315 + private volatile long highestmodseq = -1; // RFC 4551 - CONDSTORE + private boolean doExpungeNotification = true; // used in expunge handler + + private Status cachedStatus = null; + private long cachedStatusTime = 0; + + private boolean hasMessageCountListener = false; // optimize notification + + protected MailLogger logger; + private MailLogger connectionPoolLogger; + + /** + * A fetch profile item for fetching headers. + * This inner class extends the FetchProfile.Item + * class to add new FetchProfile item types, specific to IMAPFolders. + * + * @see FetchProfile + */ + public static class FetchProfileItem extends FetchProfile.Item { + protected FetchProfileItem(String name) { + super(name); + } + + /** + * HEADERS is a fetch profile item that can be included in a + * FetchProfile during a fetch request to a Folder. + * This item indicates that the headers for messages in the specified + * range are desired to be prefetched.

+ * + * An example of how a client uses this is below: + *

+	 *
+	 * 	FetchProfile fp = new FetchProfile();
+	 *	fp.add(IMAPFolder.FetchProfileItem.HEADERS);
+	 *	folder.fetch(msgs, fp);
+	 *
+	 * 
+ */ + public static final FetchProfileItem HEADERS = + new FetchProfileItem("HEADERS"); + + /** + * SIZE is a fetch profile item that can be included in a + * FetchProfile during a fetch request to a Folder. + * This item indicates that the sizes of the messages in the specified + * range are desired to be prefetched.

+ * + * SIZE was moved to FetchProfile.Item in JavaMail 1.5. + * + * @deprecated + */ + @Deprecated + public static final FetchProfileItem SIZE = + new FetchProfileItem("SIZE"); + + /** + * MESSAGE is a fetch profile item that can be included in a + * FetchProfile during a fetch request to a Folder. + * This item indicates that the entire messages (headers and body, + * including all "attachments") in the specified + * range are desired to be prefetched. Note that the entire message + * content is cached in memory while the Folder is open. The cached + * message will be parsed locally to return header information and + * message content.

+ * + * An example of how a client uses this is below: + *

+	 *
+	 * 	FetchProfile fp = new FetchProfile();
+	 *	fp.add(IMAPFolder.FetchProfileItem.MESSAGE);
+	 *	folder.fetch(msgs, fp);
+	 *
+	 * 
+ * + * @since JavaMail 1.5.2 + */ + public static final FetchProfileItem MESSAGE = + new FetchProfileItem("MESSAGE"); + + /** + * INTERNALDATE is a fetch profile item that can be included in a + * FetchProfile during a fetch request to a Folder. + * This item indicates that the IMAP INTERNALDATE values + * (received date) of the messages in the specified + * range are desired to be prefetched.

+ * + * An example of how a client uses this is below: + *

+	 *
+	 * 	FetchProfile fp = new FetchProfile();
+	 *	fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE);
+	 *	folder.fetch(msgs, fp);
+	 *
+	 * 
+ * + * @since JavaMail 1.5.5 + */ + public static final FetchProfileItem INTERNALDATE = + new FetchProfileItem("INTERNALDATE"); + } + + /** + * Constructor used to create a possibly non-existent folder. + * + * @param fullName fullname of this folder + * @param separator the default separator character for this + * folder's namespace + * @param store the Store + * @param isNamespace if this folder represents a namespace + */ + protected IMAPFolder(String fullName, char separator, IMAPStore store, + Boolean isNamespace) { + super(store); + if (fullName == null) + throw new NullPointerException("Folder name is null"); + this.fullName = fullName; + this.separator = separator; + logger = new MailLogger(this.getClass(), "DEBUG IMAP", + store.getSession().getDebug(), store.getSession().getDebugOut()); + connectionPoolLogger = store.getConnectionPoolLogger(); + + /* + * Work around apparent bug in Exchange. Exchange + * will return a name of "Public Folders/" from + * LIST "%". + * + * If name has one separator, and it's at the end, + * assume this is a namespace name and treat it + * accordingly. Usually this will happen as a result + * of the list method, but this also allows getFolder + * to work with namespace names. + */ + this.isNamespace = false; + if (separator != UNKNOWN_SEPARATOR && separator != '\0') { + int i = this.fullName.indexOf(separator); + if (i > 0 && i == this.fullName.length() - 1) { + this.fullName = this.fullName.substring(0, i); + this.isNamespace = true; + } + } + + // if we were given a value, override default chosen above + if (isNamespace != null) + this.isNamespace = isNamespace.booleanValue(); + } + + /** + * Constructor used to create an existing folder. + * + * @param li the ListInfo for this folder + * @param store the store containing this folder + */ + protected IMAPFolder(ListInfo li, IMAPStore store) { + this(li.name, li.separator, store, null); + + if (li.hasInferiors) + type |= HOLDS_FOLDERS; + if (li.canOpen) + type |= HOLDS_MESSAGES; + exists = true; + attributes = li.attrs; + } + + /* + * Ensure that this folder exists. If 'exists' has been set to true, + * we don't attempt to validate it with the server again. Note that + * this can result in a possible loss of sync with the server. + * ASSERT: Must be called with this folder's synchronization lock held. + */ + protected void checkExists() throws MessagingException { + // If the boolean field 'exists' is false, check with the + // server by invoking exists() .. + if (!exists && !exists()) + throw new FolderNotFoundException( + this, fullName + " not found"); + } + + /* + * Ensure the folder is closed. + * ASSERT: Must be called with this folder's synchronization lock held. + */ + protected void checkClosed() { + if (opened) + throw new IllegalStateException( + "This operation is not allowed on an open folder" + ); + } + + /* + * Ensure the folder is open. + * ASSERT: Must be called with this folder's synchronization lock held. + */ + protected void checkOpened() throws FolderClosedException { + assert Thread.holdsLock(this); + if (!opened) { + if (reallyClosed) + throw new IllegalStateException( + "This operation is not allowed on a closed folder" + ); + else // Folder was closed "implicitly" + throw new FolderClosedException(this, + "Lost folder connection to server" + ); + } + } + + /* + * Check that the given message number is within the range + * of messages present in this folder. If the message + * number is out of range, we ping the server to obtain any + * pending new message notifications from the server. + */ + protected void checkRange(int msgno) throws MessagingException { + if (msgno < 1) // message-numbers start at 1 + throw new IndexOutOfBoundsException("message number < 1"); + + if (msgno <= total) + return; + + // Out of range, let's ping the server and see if + // the server has more messages for us. + + synchronized(messageCacheLock) { // Acquire lock + try { + keepConnectionAlive(false); + } catch (ConnectionException cex) { + // Oops, lost connection + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } // Release lock + + if (msgno > total) // Still out of range ? Throw up ... + throw new IndexOutOfBoundsException(msgno + " > " + total); + } + + /* + * Check whether the given flags are supported by this server, + * and also verify that the folder allows setting flags. + */ + private void checkFlags(Flags flags) throws MessagingException { + assert Thread.holdsLock(this); + if (mode != READ_WRITE) + throw new IllegalStateException( + "Cannot change flags on READ_ONLY folder: " + fullName + ); + /* + if (!availableFlags.contains(flags)) + throw new MessagingException( + "These flags are not supported by this implementation" + ); + */ + } + + /** + * Get the name of this folder. + */ + @Override + public synchronized String getName() { + /* Return the last component of this Folder's full name. + * Folder components are delimited by the separator character. + */ + if (name == null) { + try { + name = fullName.substring( + fullName.lastIndexOf(getSeparator()) + 1 + ); + } catch (MessagingException mex) { } + } + return name; + } + + /** + * Get the fullname of this folder. + */ + @Override + public String getFullName() { + return fullName; + } + + /** + * Get this folder's parent. + */ + @Override + public synchronized Folder getParent() throws MessagingException { + char c = getSeparator(); + int index; + if ((index = fullName.lastIndexOf(c)) != -1) + return ((IMAPStore)store).newIMAPFolder( + fullName.substring(0, index), c); + else + return new DefaultFolder((IMAPStore)store); + } + + /** + * Check whether this folder really exists on the server. + */ + @Override + public synchronized boolean exists() throws MessagingException { + // Check whether this folder exists .. + ListInfo[] li = null; + final String lname; + if (isNamespace && separator != '\0') + lname = fullName + separator; + else + lname = fullName; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.list("", lname); + } + }); + + if (li != null) { + int i = findName(li, lname); + fullName = li[i].name; + separator = li[i].separator; + int len = fullName.length(); + if (separator != '\0' && len > 0 && + fullName.charAt(len - 1) == separator) { + fullName = fullName.substring(0, len - 1); + } + type = 0; + if (li[i].hasInferiors) + type |= HOLDS_FOLDERS; + if (li[i].canOpen) + type |= HOLDS_MESSAGES; + exists = true; + attributes = li[i].attrs; + } else { + exists = opened; + attributes = null; + } + + return exists; + } + + /** + * Which entry in li matches lname? + * If the name contains wildcards, more than one entry may be + * returned. + */ + private int findName(ListInfo[] li, String lname) { + int i; + // if the name contains a wildcard, there might be more than one + for (i = 0; i < li.length; i++) { + if (li[i].name.equals(lname)) + break; + } + if (i >= li.length) { // nothing matched exactly + // XXX - possibly should fail? But what if server + // is case insensitive and returns the preferred + // case of the name here? + i = 0; // use first one + } + return i; + } + + /** + * List all subfolders matching the specified pattern. + */ + @Override + public Folder[] list(String pattern) throws MessagingException { + return doList(pattern, false); + } + + /** + * List all subscribed subfolders matching the specified pattern. + */ + @Override + public Folder[] listSubscribed(String pattern) throws MessagingException { + return doList(pattern, true); + } + + private synchronized Folder[] doList(final String pattern, + final boolean subscribed) throws MessagingException { + checkExists(); // insure that this folder does exist. + + // Why waste a roundtrip to the server? + if (attributes != null && !isDirectory()) + return new Folder[0]; + + final char c = getSeparator(); + + ListInfo[] li = (ListInfo[])doCommandIgnoreFailure( + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + if (subscribed) + return p.lsub("", fullName + c + pattern); + else + return p.list("", fullName + c + pattern); + } + }); + + if (li == null) + return new Folder[0]; + + /* + * The UW based IMAP4 servers (e.g. SIMS2.0) include + * current folder (terminated with the separator), when + * the LIST pattern is '%' or '*'. i.e, + * returns "mail/" as the first LIST response. + * + * Doesn't make sense to include the current folder in this + * case, so we filter it out. Note that I'm assuming that + * the offending response is the *first* one, my experiments + * with the UW & SIMS2.0 servers indicate that .. + */ + int start = 0; + // Check the first LIST response. + if (li.length > 0 && li[0].name.equals(fullName + c)) + start = 1; // start from index = 1 + + IMAPFolder[] folders = new IMAPFolder[li.length - start]; + IMAPStore st = (IMAPStore)store; + for (int i = start; i < li.length; i++) + folders[i-start] = st.newIMAPFolder(li[i]); + return folders; + } + + /** + * Get the separator character. + */ + @Override + public synchronized char getSeparator() throws MessagingException { + if (separator == UNKNOWN_SEPARATOR) { + ListInfo[] li = null; + + li = (ListInfo[])doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + // REV1 allows the following LIST format to obtain + // the hierarchy delimiter of non-existent folders + if (p.isREV1()) // IMAP4rev1 + return p.list(fullName, ""); + else // IMAP4, note that this folder must exist for this + // to work :( + return p.list("", fullName); + } + }); + + if (li != null) + separator = li[0].separator; + else + separator = '/'; // punt ! + } + return separator; + } + + /** + * Get the type of this folder. + */ + @Override + public synchronized int getType() throws MessagingException { + if (opened) { + // never throw FolderNotFoundException if folder is open + if (attributes == null) + exists(); // try to fetch attributes + } else { + checkExists(); + } + return type; + } + + /** + * Check whether this folder is subscribed. + */ + @Override + public synchronized boolean isSubscribed() { + ListInfo[] li = null; + final String lname; + if (isNamespace && separator != '\0') + lname = fullName + separator; + else + lname = fullName; + + try { + li = (ListInfo[])doProtocolCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.lsub("", lname); + } + }); + } catch (ProtocolException pex) { + } + + if (li != null) { + int i = findName(li, lname); + return li[i].canOpen; + } else + return false; + } + + /** + * Subscribe/Unsubscribe this folder. + */ + @Override + public synchronized void setSubscribed(final boolean subscribe) + throws MessagingException { + doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + if (subscribe) + p.subscribe(fullName); + else + p.unsubscribe(fullName); + return null; + } + }); + } + + /** + * Create this folder, with the specified type. + */ + @Override + public synchronized boolean create(final int type) + throws MessagingException { + + char c = 0; + if ((type & HOLDS_MESSAGES) == 0) // only holds folders + c = getSeparator(); + final char sep = c; + Object ret = doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + if ((type & HOLDS_MESSAGES) == 0) // only holds folders + p.create(fullName + sep); + else { + p.create(fullName); + + // Certain IMAP servers do not allow creation of folders + // that can contain messages *and* subfolders. So, if we + // were asked to create such a folder, we should verify + // that we could indeed do so. + if ((type & HOLDS_FOLDERS) != 0) { + // we want to hold subfolders and messages. Check + // whether we could create such a folder. + ListInfo[] li = p.list("", fullName); + if (li != null && !li[0].hasInferiors) { + // Hmm ..the new folder + // doesn't support Inferiors ? Fail + p.delete(fullName); + throw new ProtocolException("Unsupported type"); + } + } + } + return Boolean.TRUE; + } + }); + + if (ret == null) + return false; // CREATE failure, maybe this + // folder already exists ? + + // exists = true; + // this.type = type; + boolean retb = exists(); // set exists, type, and attributes + if (retb) // Notify listeners on self and our Store + notifyFolderListeners(FolderEvent.CREATED); + return retb; + } + + /** + * Check whether this folder has new messages. + */ + @Override + public synchronized boolean hasNewMessages() throws MessagingException { + synchronized (messageCacheLock) { + if (opened) { // If we are open, we already have this information + // Folder is open, make sure information is up to date + // tickle the folder and store connections. + try { + keepConnectionAlive(true); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return recent > 0 ? true : false; + } + } + + // First, the cheap way - use LIST and look for the \Marked + // or \Unmarked tag + + ListInfo[] li = null; + final String lname; + if (isNamespace && separator != '\0') + lname = fullName + separator; + else + lname = fullName; + li = (ListInfo[])doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + return p.list("", lname); + } + }); + + // if folder doesn't exist, throw exception + if (li == null) + throw new FolderNotFoundException(this, fullName + " not found"); + + int i = findName(li, lname); + if (li[i].changeState == ListInfo.CHANGED) + return true; + else if (li[i].changeState == ListInfo.UNCHANGED) + return false; + + // LIST didn't work. Try the hard way, using STATUS + try { + Status status = getStatus(); + if (status.recent > 0) + return true; + else + return false; + } catch (BadCommandException bex) { + // Probably doesn't support STATUS, tough luck. + return false; + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the named subfolder. + */ + @Override + public synchronized Folder getFolder(String name) + throws MessagingException { + // If we know that this folder is *not* a directory, don't + // send the request to the server at all ... + if (attributes != null && !isDirectory()) + throw new MessagingException("Cannot contain subfolders"); + + char c = getSeparator(); + return ((IMAPStore)store).newIMAPFolder(fullName + c + name, c); + } + + /** + * Delete this folder. + */ + @Override + public synchronized boolean delete(boolean recurse) + throws MessagingException { + checkClosed(); // insure that this folder is closed. + + if (recurse) { + // Delete all subfolders. + Folder[] f = list(); + for (int i = 0; i < f.length; i++) + f[i].delete(recurse); // ignore intermediate failures + } + + // Attempt to delete this folder + + Object ret = doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + p.delete(fullName); + return Boolean.TRUE; + } + }); + + if (ret == null) + // Non-existent folder/No permission ?? + return false; + + // DELETE succeeded. + exists = false; + attributes = null; + + // Notify listeners on self and our Store + notifyFolderListeners(FolderEvent.DELETED); + return true; + } + + /** + * Rename this folder. + */ + @Override + public synchronized boolean renameTo(final Folder f) + throws MessagingException { + checkClosed(); // insure that we are closed. + checkExists(); + if (f.getStore() != store) + throw new MessagingException("Can't rename across Stores"); + + + Object ret = doCommandIgnoreFailure(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) throws ProtocolException { + p.rename(fullName, f.getFullName()); + return Boolean.TRUE; + } + }); + + if (ret == null) + return false; + + exists = false; + attributes = null; + notifyFolderRenamedListeners(f); + return true; + } + + /** + * Open this folder in the given mode. + */ + @Override + public synchronized void open(int mode) throws MessagingException { + open(mode, null); + } + + /** + * Open this folder in the given mode, with the given + * resynchronization data. + * + * @param mode the open mode (Folder.READ_WRITE or Folder.READ_ONLY) + * @param rd the ResyncData instance + * @return a List of MailEvent instances, or null if none + * @exception MessagingException if the open fails + * @since JavaMail 1.5.1 + */ + public synchronized List open(int mode, ResyncData rd) + throws MessagingException { + checkClosed(); // insure that we are not already open + + MailboxInfo mi = null; + // Request store for our own protocol connection. + protocol = ((IMAPStore)store).getProtocol(this); + + List openEvents = null; + synchronized(messageCacheLock) { // Acquire messageCacheLock + + /* + * Add response handler right away so we get any alerts or + * notifications that occur during the SELECT or EXAMINE. + * Have to be sure to remove it if we fail to open the + * folder. + */ + protocol.addResponseHandler(this); + + try { + /* + * Enable QRESYNC or CONDSTORE if needed and not enabled. + * QRESYNC implies CONDSTORE, but servers that support + * QRESYNC are not required to support just CONDSTORE + * per RFC 5162. + */ + if (rd != null) { + if (rd == ResyncData.CONDSTORE) { + if (!protocol.isEnabled("CONDSTORE") && + !protocol.isEnabled("QRESYNC")) { + if (protocol.hasCapability("CONDSTORE")) + protocol.enable("CONDSTORE"); + else + protocol.enable("QRESYNC"); + } + } else { + if (!protocol.isEnabled("QRESYNC")) + protocol.enable("QRESYNC"); + } + } + + if (mode == READ_ONLY) + mi = protocol.examine(fullName, rd); + else + mi = protocol.select(fullName, rd); + } catch (CommandFailedException cex) { + /* + * Handle SELECT or EXAMINE failure. + * Try to figure out why the operation failed so we can + * report a more reasonable exception. + * + * Will use our existing protocol object. + */ + try { + checkExists(); // throw exception if folder doesn't exist + + if ((type & HOLDS_MESSAGES) == 0) + throw new MessagingException( + "folder cannot contain messages"); + throw new MessagingException(cex.getMessage(), cex); + + } finally { + // folder not open, don't keep this information + exists = false; + attributes = null; + type = 0; + // connection still good, return it + releaseProtocol(true); + } + // NOTREACHED + } catch (ProtocolException pex) { + // got a BAD or a BYE; connection may be bad, close it + try { + throw logoutAndThrow(pex.getMessage(), pex); + } finally { + releaseProtocol(false); + } + } + + if (mi.mode != mode) { + if (mode == READ_WRITE && mi.mode == READ_ONLY && + ((IMAPStore)store).allowReadOnlySelect()) { + ; // all ok, allow it + } else { // otherwise, it's an error + ReadOnlyFolderException ife = new ReadOnlyFolderException( + this, "Cannot open in desired mode"); + throw cleanupAndThrow(ife); + } + } + + // Initialize stuff. + opened = true; + reallyClosed = false; + this.mode = mi.mode; + availableFlags = mi.availableFlags; + permanentFlags = mi.permanentFlags; + total = realTotal = mi.total; + recent = mi.recent; + uidvalidity = mi.uidvalidity; + uidnext = mi.uidnext; + uidNotSticky = mi.uidNotSticky; + highestmodseq = mi.highestmodseq; + + // Create the message cache of appropriate size + messageCache = new MessageCache(this, (IMAPStore)store, total); + + // process saved responses and return corresponding events + if (mi.responses != null) { + openEvents = new ArrayList<>(); + for (IMAPResponse ir : mi.responses) { + if (ir.keyEquals("VANISHED")) { + // "VANISHED" SP ["(EARLIER)"] SP known-uids + String[] s = ir.readAtomStringList(); + // check that it really is "EARLIER" + if (s == null || s.length != 1 || + !s[0].equalsIgnoreCase("EARLIER")) + continue; // it's not, what to do with it here? + String uids = ir.readAtom(); + UIDSet[] uidset = UIDSet.parseUIDSets(uids); + long[] luid = UIDSet.toArray(uidset, uidnext); + if (luid != null && luid.length > 0) + openEvents.add( + new MessageVanishedEvent(this, luid)); + } else if (ir.keyEquals("FETCH")) { + assert ir instanceof FetchResponse : + "!ir instanceof FetchResponse"; + Message msg = processFetchResponse((FetchResponse)ir); + if (msg != null) + openEvents.add(new MessageChangedEvent(this, + MessageChangedEvent.FLAGS_CHANGED, msg)); + } + } + } + } // Release lock + + exists = true; // if we opened it, it must exist + attributes = null; // but we don't yet know its attributes + type = HOLDS_MESSAGES; // lacking more info, we know at least this much + + // notify listeners + notifyConnectionListeners(ConnectionEvent.OPENED); + + return openEvents; + } + + private MessagingException cleanupAndThrow(MessagingException ife) { + try { + try { + // close mailbox and return connection + protocol.close(); + releaseProtocol(true); + } catch (ProtocolException pex) { + // something went wrong, close connection + try { + addSuppressed(ife, logoutAndThrow(pex.getMessage(), pex)); + } finally { + releaseProtocol(false); + } + } + } catch (Throwable thr) { + addSuppressed(ife, thr); + } + return ife; + } + + private MessagingException logoutAndThrow(String why, ProtocolException t) { + MessagingException ife = new MessagingException(why, t); + try { + protocol.logout(); + } catch (Throwable thr) { + addSuppressed(ife, thr); + } + return ife; + } + + private void addSuppressed(Throwable ife, Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + + private boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + /** + * Prefetch attributes, based on the given FetchProfile. + */ + @Override + public synchronized void fetch(Message[] msgs, FetchProfile fp) + throws MessagingException { + // cache this information in case connection is closed and + // protocol is set to null + boolean isRev1; + FetchItem[] fitems; + synchronized (messageCacheLock) { + checkOpened(); + isRev1 = protocol.isREV1(); + fitems = protocol.getFetchItems(); + } + + StringBuilder command = new StringBuilder(); + boolean first = true; + boolean allHeaders = false; + + if (fp.contains(FetchProfile.Item.ENVELOPE)) { + command.append(getEnvelopeCommand()); + first = false; + } + if (fp.contains(FetchProfile.Item.FLAGS)) { + command.append(first ? "FLAGS" : " FLAGS"); + first = false; + } + if (fp.contains(FetchProfile.Item.CONTENT_INFO)) { + command.append(first ? "BODYSTRUCTURE" : " BODYSTRUCTURE"); + first = false; + } + if (fp.contains(UIDFolder.FetchProfileItem.UID)) { + command.append(first ? "UID" : " UID"); + first = false; + } + if (fp.contains(IMAPFolder.FetchProfileItem.HEADERS)) { + allHeaders = true; + if (isRev1) + command.append(first ? + "BODY.PEEK[HEADER]" : " BODY.PEEK[HEADER]"); + else + command.append(first ? "RFC822.HEADER" : " RFC822.HEADER"); + first = false; + } + if (fp.contains(IMAPFolder.FetchProfileItem.MESSAGE)) { + allHeaders = true; + if (isRev1) + command.append(first ? "BODY.PEEK[]" : " BODY.PEEK[]"); + else + command.append(first ? "RFC822" : " RFC822"); + first = false; + } + if (fp.contains(FetchProfile.Item.SIZE) || + fp.contains(IMAPFolder.FetchProfileItem.SIZE)) { + command.append(first ? "RFC822.SIZE" : " RFC822.SIZE"); + first = false; + } + if (fp.contains(IMAPFolder.FetchProfileItem.INTERNALDATE)) { + command.append(first ? "INTERNALDATE" : " INTERNALDATE"); + first = false; + } + + // if we're not fetching all headers, fetch individual headers + String[] hdrs = null; + if (!allHeaders) { + hdrs = fp.getHeaderNames(); + if (hdrs.length > 0) { + if (!first) + command.append(" "); + command.append(createHeaderCommand(hdrs, isRev1)); + } + } + + /* + * Add any additional extension fetch items. + */ + for (int i = 0; i < fitems.length; i++) { + if (fp.contains(fitems[i].getFetchProfileItem())) { + if (command.length() != 0) + command.append(" "); + command.append(fitems[i].getName()); + } + } + + Utility.Condition condition = + new IMAPMessage.FetchProfileCondition(fp, fitems); + + // Acquire the Folder's MessageCacheLock. + synchronized (messageCacheLock) { + + // check again to make sure folder is still open + checkOpened(); + + // Apply the test, and get the sequence-number set for + // the messages that need to be prefetched. + MessageSet[] msgsets = Utility.toMessageSetSorted(msgs, condition); + + if (msgsets == null) + // We already have what we need. + return; + + Response[] r = null; + // to collect non-FETCH responses & unsolicited FETCH FLAG responses + List v = new ArrayList<>(); + try { + r = getProtocol().fetch(msgsets, command.toString()); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (CommandFailedException cfx) { + // Ignore these, as per RFC 2180 + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + + if (r == null) + return; + + for (int i = 0; i < r.length; i++) { + if (r[i] == null) + continue; + if (!(r[i] instanceof FetchResponse)) { + v.add(r[i]); // Unsolicited Non-FETCH response + continue; + } + + // Got a FetchResponse. + FetchResponse f = (FetchResponse)r[i]; + // Get the corresponding message. + IMAPMessage msg = getMessageBySeqNumber(f.getNumber()); + + int count = f.getItemCount(); + boolean unsolicitedFlags = false; + + for (int j = 0; j < count; j++) { + Item item = f.getItem(j); + // Check for the FLAGS item + if (item instanceof Flags && + (!fp.contains(FetchProfile.Item.FLAGS) || + msg == null)) { + // Ok, Unsolicited FLAGS update. + unsolicitedFlags = true; + } else if (msg != null) + msg.handleFetchItem(item, hdrs, allHeaders); + } + if (msg != null) + msg.handleExtensionFetchItems(f.getExtensionItems()); + + // If this response contains any unsolicited FLAGS + // add it to the unsolicited response vector + if (unsolicitedFlags) + v.add(f); + } + + // Dispatch any unsolicited responses + if (!v.isEmpty()) { + Response[] responses = new Response[v.size()]; + v.toArray(responses); + handleResponses(responses); + } + + } // Release messageCacheLock + } + + /** + * Return the IMAP FETCH items to request in order to load + * all the "envelope" data. Subclasses can override this + * method to fetch more data when FetchProfile.Item.ENVELOPE + * is requested. + * + * @return the IMAP FETCH items to request + * @since JavaMail 1.4.6 + */ + protected String getEnvelopeCommand() { + return IMAPMessage.EnvelopeCmd; + } + + /** + * Create a new IMAPMessage object to represent the given message number. + * Subclasses of IMAPFolder may override this method to create a + * subclass of IMAPMessage. + * + * @param msgnum the message sequence number + * @return the new IMAPMessage object + * @since JavaMail 1.4.6 + */ + protected IMAPMessage newIMAPMessage(int msgnum) { + return new IMAPMessage(this, msgnum); + } + + /** + * Create the appropriate IMAP FETCH command items to fetch the + * requested headers. + */ + private String createHeaderCommand(String[] hdrs, boolean isRev1) { + StringBuilder sb; + + if (isRev1) + sb = new StringBuilder("BODY.PEEK[HEADER.FIELDS ("); + else + sb = new StringBuilder("RFC822.HEADER.LINES ("); + + for (int i = 0; i < hdrs.length; i++) { + if (i > 0) + sb.append(" "); + sb.append(hdrs[i]); + } + + if (isRev1) + sb.append(")]"); + else + sb.append(")"); + + return sb.toString(); + } + + /** + * Set the specified flags for the given array of messages. + */ + @Override + public synchronized void setFlags(Message[] msgs, Flags flag, boolean value) + throws MessagingException { + checkOpened(); + checkFlags(flag); // validate flags + + if (msgs.length == 0) // boundary condition + return; + + synchronized(messageCacheLock) { + try { + IMAPProtocol p = getProtocol(); + MessageSet[] ms = Utility.toMessageSetSorted(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + p.storeFlags(ms, flag, value); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Set the specified flags for the given range of message numbers. + */ + @Override + public synchronized void setFlags(int start, int end, + Flags flag, boolean value) throws MessagingException { + checkOpened(); + Message[] msgs = new Message[end - start + 1]; + int i = 0; + for (int n = start; n <= end; n++) + msgs[i++] = getMessage(n); + setFlags(msgs, flag, value); + } + + /** + * Set the specified flags for the given array of message numbers. + */ + @Override + public synchronized void setFlags(int[] msgnums, Flags flag, boolean value) + throws MessagingException { + checkOpened(); + Message[] msgs = new Message[msgnums.length]; + for (int i = 0; i < msgnums.length; i++) + msgs[i] = getMessage(msgnums[i]); + setFlags(msgs, flag, value); + } + + /** + * Close this folder. + */ + @Override + public synchronized void close(boolean expunge) throws MessagingException { + close(expunge, false); + } + + /** + * Close this folder without waiting for the server. + * + * @exception MessagingException for failures + */ + public synchronized void forceClose() throws MessagingException { + close(false, true); + } + + /* + * Common close method. + */ + private void close(boolean expunge, boolean force) + throws MessagingException { + assert Thread.holdsLock(this); + synchronized(messageCacheLock) { + /* + * If we already know we're closed, this is illegal. + * Can't use checkOpened() because if we were forcibly + * closed asynchronously we just want to complete the + * closing here. + */ + if (!opened && reallyClosed) + throw new IllegalStateException( + "This operation is not allowed on a closed folder" + ); + + reallyClosed = true; // Ok, lets reset + + // Maybe this folder is already closed, or maybe another + // thread which had the messageCacheLock earlier, found + // that our server connection is dead and cleaned up + // everything .. + if (!opened) + return; + + boolean reuseProtocol = true; + try { + waitIfIdle(); + if (force) { + logger.log(Level.FINE, "forcing folder {0} to close", + fullName); + if (protocol != null) + protocol.disconnect(); + } else if (((IMAPStore)store).isConnectionPoolFull()) { + // If the connection pool is full, logout the connection + logger.fine( + "pool is full, not adding an Authenticated connection"); + + // If the expunge flag is set, close the folder first. + if (expunge && protocol != null) + protocol.close(); + + if (protocol != null) + protocol.logout(); + } else { + // If the expunge flag is set or we're open read-only we + // can just close the folder, otherwise open it read-only + // before closing, or unselect it if supported. + if (!expunge && mode == READ_WRITE) { + try { + if (protocol != null && + protocol.hasCapability("UNSELECT")) + protocol.unselect(); + else { + // Unselect isn't supported so we need to + // select a folder to cause this one to be + // deselected without expunging messages. + // We try to do that by reopening the current + // folder read-only. If the current folder + // was renamed out from under us, the EXAMINE + // might fail, but that's ok because it still + // leaves us with the folder deselected. + if (protocol != null) { + boolean selected = true; + try { + protocol.examine(fullName); + // success, folder still selected + } catch (CommandFailedException ex) { + // EXAMINE failed, folder is no + // longer selected + selected = false; + } + if (selected && protocol != null) + protocol.close(); + } + } + } catch (ProtocolException pex2) { + reuseProtocol = false; // something went wrong + } + } else { + if (protocol != null) + protocol.close(); + } + } + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + // cleanup if we haven't already + if (opened) + cleanup(reuseProtocol); + } + } + } + + // NOTE: this method can currently be invoked from close() or + // from handleResponses(). Both invocations are conditional, + // based on the "opened" flag, so we are sure that multiple + // Connection.CLOSED events are not generated. Also both + // invocations are from within messageCacheLock-ed areas. + private void cleanup(boolean returnToPool) { + assert Thread.holdsLock(messageCacheLock); + releaseProtocol(returnToPool); + messageCache = null; + uidTable = null; + exists = false; // to force a recheck in exists(). + attributes = null; + opened = false; + idleState = RUNNING; // just in case + messageCacheLock.notifyAll(); // wake up anyone waiting + notifyConnectionListeners(ConnectionEvent.CLOSED); + } + + /** + * Check whether this connection is really open. + */ + @Override + public synchronized boolean isOpen() { + synchronized(messageCacheLock) { + // Probe the connection to make sure its really open. + if (opened) { + try { + keepConnectionAlive(false); + } catch (ProtocolException pex) { } + } + } + + return opened; + } + + /** + * Return the permanent flags supported by the server. + */ + @Override + public synchronized Flags getPermanentFlags() { + if (permanentFlags == null) + return null; + return (Flags)(permanentFlags.clone()); + } + + /** + * Get the total message count. + */ + @Override + public synchronized int getMessageCount() throws MessagingException { + synchronized (messageCacheLock) { + if (opened) { + // Folder is open, we know what the total message count is .. + // tickle the folder and store connections. + try { + keepConnectionAlive(true); + return total; + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + // If this folder is not yet open, we use STATUS to + // get the total message count + checkExists(); + try { + Status status = getStatus(); + return status.total; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // lets try EXAMINE + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); // XXX + MailboxInfo minfo = p.examine(fullName); + p.close(); + return minfo.total; + } catch (ProtocolException pex) { + // Give up. + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the new message count. + */ + @Override + public synchronized int getNewMessageCount() throws MessagingException { + synchronized (messageCacheLock) { + if (opened) { + // Folder is open, we know what the new message count is .. + // tickle the folder and store connections. + try { + keepConnectionAlive(true); + return recent; + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + // If this folder is not yet open, we use STATUS to + // get the new message count + checkExists(); + try { + Status status = getStatus(); + return status.recent; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // lets try EXAMINE + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); // XXX + MailboxInfo minfo = p.examine(fullName); + p.close(); + return minfo.recent; + } catch (ProtocolException pex) { + // Give up. + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the unread message count. + */ + @Override + public synchronized int getUnreadMessageCount() + throws MessagingException { + if (!opened) { + checkExists(); + // If this folder is not yet open, we use STATUS to + // get the unseen message count + try { + Status status = getStatus(); + return status.unseen; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // Could EXAMINE, SEARCH for UNREAD messages and + // return the count .. bah, not worth it. + return -1; + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + // if opened, issue server-side search for messages that do + // *not* have the SEEN flag. + Flags f = new Flags(); + f.add(Flags.Flag.SEEN); + try { + synchronized(messageCacheLock) { + int[] matches = getProtocol().search(new FlagTerm(f, false)); + return matches.length; // NOTE: 'matches' is never null + } + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // Shouldn't happen + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the deleted message count. + */ + @Override + public synchronized int getDeletedMessageCount() + throws MessagingException { + if (!opened) { + checkExists(); + // no way to do this on closed folders + return -1; + } + + // if opened, issue server-side search for messages that do + // have the DELETED flag. + Flags f = new Flags(); + f.add(Flags.Flag.DELETED); + try { + synchronized(messageCacheLock) { + int[] matches = getProtocol().search(new FlagTerm(f, true)); + return matches.length; // NOTE: 'matches' is never null + } + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // Shouldn't happen + throw new MessagingException(pex.getMessage(), pex); + } + } + + /* + * Get results of STATUS command for this folder, checking cache first. + * ASSERT: Must be called with this folder's synchronization lock held. + * ASSERT: The folder must be closed. + */ + private Status getStatus() throws ProtocolException { + int statusCacheTimeout = ((IMAPStore)store).getStatusCacheTimeout(); + + // if allowed to cache and our cache is still valid, return it + if (statusCacheTimeout > 0 && cachedStatus != null && + System.currentTimeMillis() - cachedStatusTime < statusCacheTimeout) + return cachedStatus; + + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); // XXX + Status s = p.status(fullName, null); + // if allowed to cache, do so + if (statusCacheTimeout > 0) { + cachedStatus = s; + cachedStatusTime = System.currentTimeMillis(); + } + return s; + } finally { + releaseStoreProtocol(p); + } + } + + /** + * Get the specified message. + */ + @Override + public synchronized Message getMessage(int msgnum) + throws MessagingException { + checkOpened(); + checkRange(msgnum); + + return messageCache.getMessage(msgnum); + } + + /** + * {@inheritDoc} + */ + @Override + public synchronized Message[] getMessages() throws MessagingException { + /* + * Need to override Folder method to throw FolderClosedException + * instead of IllegalStateException if not really closed. + */ + checkOpened(); + int total = getMessageCount(); + Message[] msgs = new Message[total]; + for (int i = 1; i <= total; i++) + msgs[i - 1] = messageCache.getMessage(i); + return msgs; + } + + /** + * Append the given messages into this folder. + */ + @Override + public synchronized void appendMessages(Message[] msgs) + throws MessagingException { + checkExists(); // verify that self exists + + // XXX - have to verify that messages are in a different + // store (if any) than target folder, otherwise could + // deadlock trying to fetch messages on the same connection + // we're using for the append. + + int maxsize = ((IMAPStore)store).getAppendBufferSize(); + + for (int i = 0; i < msgs.length; i++) { + final Message m = msgs[i]; + Date d = m.getReceivedDate(); // retain dates + if (d == null) + d = m.getSentDate(); + final Date dd = d; + final Flags f = m.getFlags(); + + final MessageLiteral mos; + try { + // if we know the message is too big, don't buffer any of it + mos = new MessageLiteral(m, + m.getSize() > maxsize ? 0 : maxsize); + } catch (IOException ex) { + throw new MessagingException( + "IOException while appending messages", ex); + } catch (MessageRemovedException mrex) { + continue; // just skip this expunged message + } + + doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.append(fullName, f, dd, mos); + return null; + } + }); + } + } + + /** + * Append the given messages into this folder. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the msgs array. A null + * element means the server didn't return UID information + * for the appended message.

+ * + * Depends on the APPENDUID response code defined by the + * UIDPLUS extension - + * RFC 4315. + * + * @param msgs the messages to append + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized AppendUID[] appendUIDMessages(Message[] msgs) + throws MessagingException { + checkExists(); // verify that self exists + + // XXX - have to verify that messages are in a different + // store (if any) than target folder, otherwise could + // deadlock trying to fetch messages on the same connection + // we're using for the append. + + int maxsize = ((IMAPStore)store).getAppendBufferSize(); + + AppendUID[] uids = new AppendUID[msgs.length]; + for (int i = 0; i < msgs.length; i++) { + final Message m = msgs[i]; + final MessageLiteral mos; + + try { + // if we know the message is too big, don't buffer any of it + mos = new MessageLiteral(m, + m.getSize() > maxsize ? 0 : maxsize); + } catch (IOException ex) { + throw new MessagingException( + "IOException while appending messages", ex); + } catch (MessageRemovedException mrex) { + continue; // just skip this expunged message + } + + Date d = m.getReceivedDate(); // retain dates + if (d == null) + d = m.getSentDate(); + final Date dd = d; + final Flags f = m.getFlags(); + AppendUID auid = (AppendUID)doCommand(new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.appenduid(fullName, f, dd, mos); + } + }); + uids[i] = auid; + } + return uids; + } + + /** + * Append the given messages into this folder. + * Return array of Message objects representing + * the messages in the destination folder. Note + * that the folder must be open. + * Each element of the returned array corresponds to + * an element of the msgs array. A null + * element means the server didn't return UID information + * for the appended message.

+ * + * Depends on the APPENDUID response code defined by the + * UIDPLUS extension - + * RFC 4315. + * + * @param msgs the messages to add + * @return the messages in this folder + * @exception MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized Message[] addMessages(Message[] msgs) + throws MessagingException { + checkOpened(); + Message[] rmsgs = new MimeMessage[msgs.length]; + AppendUID[] uids = appendUIDMessages(msgs); + for (int i = 0; i < uids.length; i++) { + AppendUID auid = uids[i]; + if (auid != null) { + if (auid.uidvalidity == uidvalidity) { + try { + rmsgs[i] = getMessageByUID(auid.uid); + } catch (MessagingException mex) { + // ignore errors at this stage + } + } + } + } + return rmsgs; + } + + /** + * Copy the specified messages from this folder, to the + * specified destination. + */ + @Override + public synchronized void copyMessages(Message[] msgs, Folder folder) + throws MessagingException { + copymoveMessages(msgs, folder, false); + } + + /** + * Copy the specified messages from this folder, to the + * specified destination. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the msgs array. A null + * element means the server didn't return UID information + * for the copied message.

+ * + * Depends on the COPYUID response code defined by the + * UIDPLUS extension - + * RFC 4315. + * + * @param msgs the messages to copy + * @param folder the folder to copy the messages to + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.5.1 + */ + public synchronized AppendUID[] copyUIDMessages(Message[] msgs, + Folder folder) throws MessagingException { + return copymoveUIDMessages(msgs, folder, false); + } + + /** + * Move the specified messages from this folder, to the + * specified destination. + * + * Depends on the MOVE extension + * (RFC 6851). + * + * @param msgs the messages to move + * @param folder the folder to move the messages to + * @exception MessagingException for failures + * + * @since JavaMail 1.5.4 + */ + public synchronized void moveMessages(Message[] msgs, Folder folder) + throws MessagingException { + copymoveMessages(msgs, folder, true); + } + + /** + * Move the specified messages from this folder, to the + * specified destination. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the msgs array. A null + * element means the server didn't return UID information + * for the moved message.

+ * + * Depends on the MOVE extension + * (RFC 6851) + * and the COPYUID response code defined by the + * UIDPLUS extension + * (RFC 4315). + * + * @param msgs the messages to move + * @param folder the folder to move the messages to + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.5.4 + */ + public synchronized AppendUID[] moveUIDMessages(Message[] msgs, + Folder folder) throws MessagingException { + return copymoveUIDMessages(msgs, folder, true); + } + + /** + * Copy or move the specified messages from this folder, to the + * specified destination. + * + * @since JavaMail 1.5.4 + */ + private synchronized void copymoveMessages(Message[] msgs, Folder folder, + boolean move) throws MessagingException { + checkOpened(); + + if (msgs.length == 0) // boundary condition + return; + + // If the destination belongs to our same store, optimize + if (folder.getStore() == store) { + synchronized(messageCacheLock) { + try { + IMAPProtocol p = getProtocol(); + MessageSet[] ms = Utility.toMessageSet(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + if (move) + p.move(ms, folder.getFullName()); + else + p.copy(ms, folder.getFullName()); + } catch (CommandFailedException cfx) { + if (cfx.getMessage().indexOf("TRYCREATE") != -1) + throw new FolderNotFoundException( + folder, + folder.getFullName() + " does not exist" + ); + else + throw new MessagingException(cfx.getMessage(), cfx); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } else // destination is a different store. + if (move) + throw new MessagingException( + "Move between stores not supported"); + else + super.copyMessages(msgs, folder); + } + + /** + * Copy or move the specified messages from this folder, to the + * specified destination. + * Return array of AppendUID objects containing + * UIDs of these messages in the destination folder. + * Each element of the returned array corresponds to + * an element of the msgs array. A null + * element means the server didn't return UID information + * for the copied message.

+ * + * Depends on the COPYUID response code defined by the + * UIDPLUS extension - + * RFC 4315. + * Move depends on the MOVE extension - + * RFC 6851. + * + * @param msgs the messages to copy + * @param folder the folder to copy the messages to + * @param move move instead of copy? + * @return array of AppendUID objects + * @exception MessagingException for failures + * @since JavaMail 1.5.4 + */ + private synchronized AppendUID[] copymoveUIDMessages(Message[] msgs, + Folder folder, boolean move) throws MessagingException { + checkOpened(); + + if (msgs.length == 0) // boundary condition + return null; + + // the destination must belong to our same store + if (folder.getStore() != store) // destination is a different store. + throw new MessagingException( + move ? + "can't moveUIDMessages to a different store" : + "can't copyUIDMessages to a different store"); + + // call fetch to make sure we have all the UIDs + // necessary to interpret the COPYUID response + FetchProfile fp = new FetchProfile(); + fp.add(UIDFolder.FetchProfileItem.UID); + fetch(msgs, fp); + // XXX - could pipeline the FETCH with the COPY/MOVE below + + synchronized (messageCacheLock) { + try { + IMAPProtocol p = getProtocol(); + // XXX - messages have to be from this Folder, who checks? + MessageSet[] ms = Utility.toMessageSet(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + CopyUID cuid; + if (move) + cuid = p.moveuid(ms, folder.getFullName()); + else + cuid = p.copyuid(ms, folder.getFullName()); + + /* + * Correlate source UIDs with destination UIDs. + * This won't be time or space efficient if there's + * a lot of messages. + * + * In order to make sense of the returned UIDs, we need + * the UIDs for every one of the original messages. + * We fetch them above, to make sure we have them. + * This is critical for MOVE since after the MOVE the + * messages are gone/expunged. + * + * Assume the common case is that the messages are + * in order by UID. Map the returned source + * UIDs to their corresponding Message objects. + * Step through the msgs array looking for the + * Message object in the returned source message + * list. Most commonly the source message (UID) + * for the Nth original message will be in the Nth + * position in the returned source message (UID) + * list. Thus, the destination UID is in the Nth + * position in the returned destination UID list. + * But if the source message isn't where expected, + * we have to search the entire source message + * list, starting from where we expect it and + * wrapping around until we've searched it all. + * (Gmail will often return the lists in an unexpected order.) + * + * A possible optimization: + * If the number of UIDs returned is the same as the + * number of messages being copied/moved, we could + * sort the source messages by message number, sort + * the source and destination parallel arrays by source + * UID, and the resulting message and destination UID + * arrays will correspond. + * + * If the returned UID array size is different, some + * message was expunged while we were trying to copy/move it. + * This should be rare but would mean falling back to the + * general algorithm. + */ + long[] srcuids = UIDSet.toArray(cuid.src); + long[] dstuids = UIDSet.toArray(cuid.dst); + // map source UIDs to Message objects + // XXX - could inline/optimize this + Message[] srcmsgs = getMessagesByUID(srcuids); + AppendUID[] result = new AppendUID[msgs.length]; + for (int i = 0; i < msgs.length; i++) { + int j = i; + do { + if (msgs[i] == srcmsgs[j]) { + result[i] = new AppendUID( + cuid.uidvalidity, dstuids[j]); + break; + } + j++; + if (j >= srcmsgs.length) + j = 0; + } while (j != i); + } + return result; + } catch (CommandFailedException cfx) { + if (cfx.getMessage().indexOf("TRYCREATE") != -1) + throw new FolderNotFoundException( + folder, + folder.getFullName() + " does not exist" + ); + else + throw new MessagingException(cfx.getMessage(), cfx); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Expunge all messages marked as DELETED. + */ + @Override + public synchronized Message[] expunge() throws MessagingException { + return expunge(null); + } + + /** + * Expunge the indicated messages, which must have been marked as DELETED. + * + * Depends on the UIDPLUS extension - + * RFC 4315. + * + * @param msgs the messages to expunge + * @return the expunged messages + * @exception MessagingException for failures + */ + public synchronized Message[] expunge(Message[] msgs) + throws MessagingException { + checkOpened(); + + if (msgs != null) { + // call fetch to make sure we have all the UIDs + FetchProfile fp = new FetchProfile(); + fp.add(UIDFolder.FetchProfileItem.UID); + fetch(msgs, fp); + } + + IMAPMessage[] rmsgs; + synchronized(messageCacheLock) { + doExpungeNotification = false; // We do this ourselves later + try { + IMAPProtocol p = getProtocol(); + if (msgs != null) + p.uidexpunge(Utility.toUIDSet(msgs)); + else + p.expunge(); + } catch (CommandFailedException cfx) { + // expunge not allowed, perhaps due to a permission problem? + if (mode != READ_WRITE) + throw new IllegalStateException( + "Cannot expunge READ_ONLY folder: " + fullName); + else + throw new MessagingException(cfx.getMessage(), cfx); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // Bad bad server .. + throw new MessagingException(pex.getMessage(), pex); + } finally { + doExpungeNotification = true; + } + + // Cleanup expunged messages and sync messageCache with reality. + if (msgs != null) + rmsgs = messageCache.removeExpungedMessages(msgs); + else + rmsgs = messageCache.removeExpungedMessages(); + if (uidTable != null) { + for (int i = 0; i < rmsgs.length; i++) { + IMAPMessage m = rmsgs[i]; + /* remove this message from the UIDTable */ + long uid = m.getUID(); + if (uid != -1) + uidTable.remove(Long.valueOf(uid)); + } + } + + // Update 'total' + total = messageCache.size(); + } + + // Notify listeners. This time its for real, guys. + if (rmsgs.length > 0) + notifyMessageRemovedListeners(true, rmsgs); + return rmsgs; + } + + /** + * Search whole folder for messages matching the given term. + * If the property mail.imap.throwsearchexception is true, + * and the search term is too complex for the IMAP protocol, + * SearchException is thrown. Otherwise, if the search term is too + * complex, super.search is called to do the search on + * the client. + * + * @param term the search term + * @return the messages that match + * @exception SearchException if mail.imap.throwsearchexception is + * true and the search is too complex for the IMAP protocol + * @exception MessagingException for other failures + */ + @Override + public synchronized Message[] search(SearchTerm term) + throws MessagingException { + checkOpened(); + + try { + Message[] matchMsgs = null; + + synchronized(messageCacheLock) { + int[] matches = getProtocol().search(term); + if (matches != null) + matchMsgs = getMessagesBySeqNumbers(matches); + } + return matchMsgs; + + } catch (CommandFailedException cfx) { + // unsupported charset or search criterion + return super.search(term); + } catch (SearchException sex) { + // too complex for IMAP + if (((IMAPStore)store).throwSearchException()) + throw sex; + return super.search(term); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // bug in our IMAP layer ? + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Search the folder for messages matching the given term. Returns + * array of matching messages. Returns an empty array if no matching + * messages are found. + */ + @Override + public synchronized Message[] search(SearchTerm term, Message[] msgs) + throws MessagingException { + checkOpened(); + + if (msgs.length == 0) + // need to return an empty array (not null!) + return msgs; + + try { + Message[] matchMsgs = null; + + synchronized(messageCacheLock) { + IMAPProtocol p = getProtocol(); + MessageSet[] ms = Utility.toMessageSetSorted(msgs, null); + if (ms == null) + throw new MessageRemovedException( + "Messages have been removed"); + int[] matches = p.search(ms, term); + if (matches != null) + matchMsgs = getMessagesBySeqNumbers(matches); + } + return matchMsgs; + + } catch (CommandFailedException cfx) { + // unsupported charset or search criterion + return super.search(term, msgs); + } catch (SearchException sex) { + // too complex for IMAP + return super.search(term, msgs); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // bug in our IMAP layer ? + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Sort the messages in the folder according to the sort criteria. + * The messages are returned in the sorted order, but the order of + * the messages in the folder is not changed.

+ * + * Depends on the SORT extension - + * RFC 5256. + * + * @param term the SortTerms + * @return the messages in sorted order + * @exception MessagingException for failures + * @since JavaMail 1.4.4 + */ + public synchronized Message[] getSortedMessages(SortTerm[] term) + throws MessagingException { + return getSortedMessages(term, null); + } + + /** + * Sort the messages in the folder according to the sort criteria. + * The messages are returned in the sorted order, but the order of + * the messages in the folder is not changed. Only messages matching + * the search criteria are considered.

+ * + * Depends on the SORT extension - + * RFC 5256. + * + * @param term the SortTerms + * @param sterm the SearchTerm + * @return the messages in sorted order + * @exception MessagingException for failures + * @since JavaMail 1.4.4 + */ + public synchronized Message[] getSortedMessages(SortTerm[] term, + SearchTerm sterm) throws MessagingException { + checkOpened(); + + try { + Message[] matchMsgs = null; + + synchronized(messageCacheLock) { + int[] matches = getProtocol().sort(term, sterm); + if (matches != null) + matchMsgs = getMessagesBySeqNumbers(matches); + } + return matchMsgs; + + } catch (CommandFailedException cfx) { + // unsupported charset or search criterion + throw new MessagingException(cfx.getMessage(), cfx); + } catch (SearchException sex) { + // too complex for IMAP + throw new MessagingException(sex.getMessage(), sex); + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + // bug in our IMAP layer ? + throw new MessagingException(pex.getMessage(), pex); + } + } + + /* + * Override Folder method to keep track of whether we have any + * message count listeners. Normally we won't have any, so we + * can avoid creating message objects to pass to the notify + * method. It's too hard to keep track of when all listeners + * are removed, and that's a rare case, so we don't try. + */ + @Override + public synchronized void addMessageCountListener(MessageCountListener l) { + super.addMessageCountListener(l); + hasMessageCountListener = true; + } + + /*********************************************************** + * UIDFolder interface methods + **********************************************************/ + + /** + * Returns the UIDValidity for this folder. + */ + @Override + public synchronized long getUIDValidity() throws MessagingException { + if (opened) // we already have this information + return uidvalidity; + + IMAPProtocol p = null; + Status status = null; + + try { + p = getStoreProtocol(); // XXX + String[] item = { "UIDVALIDITY" }; + status = p.status(fullName, item); + } catch (BadCommandException bex) { + // Probably a RFC1730 server + throw new MessagingException("Cannot obtain UIDValidity", bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + + if (status == null) + throw new MessagingException("Cannot obtain UIDValidity"); + return status.uidvalidity; + } + + /** + * Returns the predicted UID that will be assigned to the + * next message that is appended to this folder. + * If the folder is closed, the STATUS command is used to + * retrieve this value. If the folder is open, the value + * returned from the SELECT or EXAMINE command is returned. + * Note that messages may have been appended to the folder + * while it was open and thus this value may be out of + * date.

+ * + * Servers implementing RFC2060 likely won't return this value + * when a folder is opened. Servers implementing RFC3501 + * should return this value when a folder is opened.

+ * + * @return the UIDNEXT value, or -1 if unknown + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + @Override + public synchronized long getUIDNext() throws MessagingException { + if (opened) // we already have this information + return uidnext; + + IMAPProtocol p = null; + Status status = null; + + try { + p = getStoreProtocol(); // XXX + String[] item = { "UIDNEXT" }; + status = p.status(fullName, item); + } catch (BadCommandException bex) { + // Probably a RFC1730 server + throw new MessagingException("Cannot obtain UIDNext", bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + + if (status == null) + throw new MessagingException("Cannot obtain UIDNext"); + return status.uidnext; + } + + /** + * Get the Message corresponding to the given UID. + * If no such message exists, null is returned. + */ + @Override + public synchronized Message getMessageByUID(long uid) + throws MessagingException { + checkOpened(); // insure folder is open + + IMAPMessage m = null; + + try { + synchronized(messageCacheLock) { + Long l = Long.valueOf(uid); + + if (uidTable != null) { + // Check in uidTable + m = uidTable.get(l); + if (m != null) // found it + return m; + } else + uidTable = new Hashtable<>(); + + // Check with the server + // Issue UID FETCH command + getProtocol().fetchSequenceNumber(uid); + + if (uidTable != null) { + // Check in uidTable + m = uidTable.get(l); + if (m != null) // found it + return m; + } + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + + return m; + } + + /** + * Get the Messages specified by the given range.

+ * Returns Message objects for all valid messages in this range. + * Returns an empty array if no messages are found. + */ + @Override + public synchronized Message[] getMessagesByUID(long start, long end) + throws MessagingException { + checkOpened(); // insure that folder is open + + Message[] msgs; // array of messages to be returned + + try { + synchronized(messageCacheLock) { + if (uidTable == null) + uidTable = new Hashtable<>(); + + // Issue UID FETCH for given range + long[] ua = getProtocol().fetchSequenceNumbers(start, end); + + List ma = new ArrayList<>(); + // NOTE: Below must be within messageCacheLock region + for (int i = 0; i < ua.length; i++) { + Message m = uidTable.get(Long.valueOf(ua[i])); + if (m != null) // found it + ma.add(m); + } + msgs = ma.toArray(new Message[ma.size()]); + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + + return msgs; + } + + /** + * Get the Messages specified by the given array.

+ * + * uids.length() elements are returned. + * If any UID in the array is invalid, a null entry + * is returned for that element. + */ + @Override + public synchronized Message[] getMessagesByUID(long[] uids) + throws MessagingException { + checkOpened(); // insure that folder is open + + try { + synchronized(messageCacheLock) { + long[] unavailUids = uids; + if (uidTable != null) { + // to collect unavailable UIDs + List v = new ArrayList<>(); + for (long uid : uids) { + if (!uidTable.containsKey(uid)) { + // This UID has not been loaded yet. + v.add(uid); + } + } + + int vsize = v.size(); + unavailUids = new long[vsize]; + for (int i = 0; i < vsize; i++) { + unavailUids[i] = v.get(i); + } + } else + uidTable = new Hashtable<>(); + + if (unavailUids.length > 0) { + // Issue UID FETCH request for given uids + getProtocol().fetchSequenceNumbers(unavailUids); + } + + // Return array of size = uids.length + Message[] msgs = new Message[uids.length]; + for (int i = 0; i < uids.length; i++) + msgs[i] = (Message)uidTable.get(Long.valueOf(uids[i])); + return msgs; + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the UID for the specified message. + */ + @Override + public synchronized long getUID(Message message) + throws MessagingException { + if (message.getFolder() != this) + throw new NoSuchElementException( + "Message does not belong to this folder"); + + checkOpened(); // insure that folder is open + + if (!(message instanceof IMAPMessage)) + throw new MessagingException("message is not an IMAPMessage"); + IMAPMessage m = (IMAPMessage)message; + // If the message already knows its UID, great .. + long uid; + if ((uid = m.getUID()) != -1) + return uid; + + synchronized(messageCacheLock) { // Acquire Lock + try { + IMAPProtocol p = getProtocol(); + m.checkExpunged(); // insure that message is not expunged + UID u = p.fetchUID(m.getSequenceNumber()); + + if (u != null) { + uid = u.uid; + m.setUID(uid); // set message's UID + + // insert this message into uidTable + if (uidTable == null) + uidTable = new Hashtable<>(); + uidTable.put(Long.valueOf(uid), m); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + return uid; + } + + /** + * Servers that support the UIDPLUS extension + * (RFC 4315) + * may indicate that this folder does not support persistent UIDs; + * that is, UIDVALIDITY will be different each time the folder is + * opened. Only valid when the folder is open. + * + * @return true if UIDs are not sticky + * @exception MessagingException for failures + * @exception IllegalStateException if the folder isn't open + * @see "RFC 4315" + * @since JavaMail 1.6.0 + */ + public synchronized boolean getUIDNotSticky() throws MessagingException { + checkOpened(); + return uidNotSticky; + } + + /** + * Get or create Message objects for the UIDs. + */ + private Message[] createMessagesForUIDs(long[] uids) { + IMAPMessage[] msgs = new IMAPMessage[uids.length]; + for (int i = 0; i < uids.length; i++) { + IMAPMessage m = null; + if (uidTable != null) + m = uidTable.get(Long.valueOf(uids[i])); + if (m == null) { + // fake it, we don't know what message this really is + m = newIMAPMessage(-1); // no sequence number + m.setUID(uids[i]); + m.setExpunged(true); + } + msgs[i++] = m; + } + return msgs; + } + + /** + * Returns the HIGHESTMODSEQ for this folder. + * + * @return the HIGHESTMODSEQ value + * @exception MessagingException for failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public synchronized long getHighestModSeq() throws MessagingException { + if (opened) // we already have this information + return highestmodseq; + + IMAPProtocol p = null; + Status status = null; + + try { + p = getStoreProtocol(); // XXX + if (!p.hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + String[] item = { "HIGHESTMODSEQ" }; + status = p.status(fullName, item); + } catch (BadCommandException bex) { + // Probably a RFC1730 server + throw new MessagingException("Cannot obtain HIGHESTMODSEQ", bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + + if (status == null) + throw new MessagingException("Cannot obtain HIGHESTMODSEQ"); + return status.highestmodseq; + } + + /** + * Get the messages that have been changed since the given MODSEQ value. + * Also, prefetch the flags for the messages.

+ * + * The server must support the CONDSTORE extension. + * + * @param start the first message number + * @param end the last message number + * @param modseq the MODSEQ value + * @return the changed messages + * @exception MessagingException for failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public synchronized Message[] getMessagesByUIDChangedSince( + long start, long end, long modseq) + throws MessagingException { + checkOpened(); // insure that folder is open + + try { + synchronized (messageCacheLock) { + IMAPProtocol p = getProtocol(); + if (!p.hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + + // Issue FETCH for given range + int[] nums = p.uidfetchChangedSince(start, end, modseq); + return getMessagesBySeqNumbers(nums); + } + } catch(ConnectionException cex) { + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + + /** + * Get the quotas for the quotaroot associated with this + * folder. Note that many folders may have the same quotaroot. + * Quotas are controlled on the basis of a quotaroot, not + * (necessarily) a folder. The relationship between folders + * and quotaroots depends on the IMAP server. Some servers + * might implement a single quotaroot for all folders owned by + * a user. Other servers might implement a separate quotaroot + * for each folder. A single folder can even have multiple + * quotaroots, perhaps controlling quotas for different + * resources. + * + * @return array of Quota objects for the quotaroots associated with + * this folder + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + public Quota[] getQuota() throws MessagingException { + return (Quota[])doOptionalCommand("QUOTA not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.getQuotaRoot(fullName); + } + }); + } + + /** + * Set the quotas for the quotaroot specified in the quota argument. + * Typically this will be one of the quotaroots associated with this + * folder, as obtained from the getQuota method, but it + * need not be. + * + * @param quota the quota to set + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + public void setQuota(final Quota quota) throws MessagingException { + doOptionalCommand("QUOTA not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.setQuota(quota); + return null; + } + }); + } + + /** + * Get the access control list entries for this folder. + * + * @return array of access control list entries + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public ACL[] getACL() throws MessagingException { + return (ACL[])doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.getACL(fullName); + } + }); + } + + /** + * Add an access control list entry to the access control list + * for this folder. + * + * @param acl the access control list entry to add + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void addACL(ACL acl) throws MessagingException { + setACL(acl, '\0'); + } + + /** + * Remove any access control list entry for the given identifier + * from the access control list for this folder. + * + * @param name the identifier for which to remove all ACL entries + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void removeACL(final String name) throws MessagingException { + doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.deleteACL(fullName, name); + return null; + } + }); + } + + /** + * Add the rights specified in the ACL to the entry for the + * identifier specified in the ACL. If an entry for the identifier + * doesn't already exist, add one. + * + * @param acl the identifer and rights to add + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void addRights(ACL acl) throws MessagingException { + setACL(acl, '+'); + } + + /** + * Remove the rights specified in the ACL from the entry for the + * identifier specified in the ACL. + * + * @param acl the identifer and rights to remove + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public void removeRights(ACL acl) throws MessagingException { + setACL(acl, '-'); + } + + /** + * Get all the rights that may be allowed to the given identifier. + * Rights are grouped per RFC 2086 and each group is returned as an + * element of the array. The first element of the array is the set + * of rights that are always granted to the identifier. Later + * elements are rights that may be optionally granted to the + * identifier.

+ * + * Note that this method lists the rights that it is possible to + * assign to the given identifier, not the rights that are + * actually granted to the given identifier. For the latter, see + * the getACL method. + * + * @param name the identifier to list rights for + * @return array of Rights objects representing possible + * rights for the identifier + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public Rights[] listRights(final String name) throws MessagingException { + return (Rights[])doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.listRights(fullName, name); + } + }); + } + + /** + * Get the rights allowed to the currently authenticated user. + * + * @return the rights granted to the current user + * @exception MessagingException if the server doesn't support the + * ACL extension + */ + public Rights myRights() throws MessagingException { + return (Rights)doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.myRights(fullName); + } + }); + } + + private void setACL(final ACL acl, final char mod) + throws MessagingException { + doOptionalCommand("ACL not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + p.setACL(fullName, mod, acl); + return null; + } + }); + } + + /** + * Get the attributes that the IMAP server returns with the + * LIST response. + * + * @return array of attributes for this folder + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public synchronized String[] getAttributes() throws MessagingException { + checkExists(); + if (attributes == null) + exists(); // do a LIST to set the attributes + return attributes == null ? new String[0] : attributes.clone(); + } + + /** + * Use the IMAP IDLE command (see + * RFC 2177), + * if supported by the server, to enter idle mode so that the server + * can send unsolicited notifications of new messages arriving, etc. + * without the need for the client to constantly poll the server. + * Use an appropriate listener to be notified of new messages or + * other events. When another thread (e.g., the listener thread) + * needs to issue an IMAP comand for this folder, the idle mode will + * be terminated and this method will return. Typically the caller + * will invoke this method in a loop.

+ * + * The mail.imap.minidletime property enforces a minimum delay + * before returning from this method, to ensure that other threads + * have a chance to issue commands before the caller invokes this + * method again. The default delay is 10 milliseconds. + * + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the folder isn't open + * + * @since JavaMail 1.4.1 + */ + public void idle() throws MessagingException { + idle(false); + } + + /** + * Like {@link #idle}, but if once is true, abort the + * IDLE command after the first notification, to allow the caller + * to process any notification synchronously. + * + * @param once only do one notification? + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the folder isn't open + * + * @since JavaMail 1.4.3 + */ + public void idle(boolean once) throws MessagingException { + synchronized (this) { + /* + * We can't support the idle method if we're using SocketChannels + * because SocketChannels don't allow simultaneous read and write. + * If we're blocked in a read waiting for IDLE responses, we can't + * send the DONE message to abort the IDLE. Sigh. + * XXX - We could do select here too, like IdleManager, instead + * of blocking in read, but that's more complicated. + */ + if (protocol != null && protocol.getChannel() != null) + throw new MessagingException( + "idle method not supported with SocketChannels"); + } + if (!startIdle(null)) + return; + + /* + * We gave up the folder lock so that other threads + * can get into the folder far enough to see that we're + * in IDLE and abort the IDLE. + * + * Now we read responses from the IDLE command, especially + * including unsolicited notifications from the server. + * We don't hold the messageCacheLock while reading because + * it protects the idleState and other threads need to be + * able to examine the state. + * + * The messageCacheLock is held in handleIdle while processing + * the responses so that we can update the number of messages + * in the folder (for example). + */ + for (;;) { + if (!handleIdle(once)) + break; + } + + /* + * Enforce a minimum delay to give time to threads + * processing the responses that came in while we + * were idle. + */ + int minidle = ((IMAPStore)store).getMinIdleTime(); + if (minidle > 0) { + try { + Thread.sleep(minidle); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might depend on + Thread.currentThread().interrupt(); + } + } + } + + /** + * Start the IDLE command and put this folder into the IDLE state. + * IDLE processing is done later in handleIdle(), e.g., called from + * the IdleManager. + * + * @return true if IDLE started, false otherwise + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the folder isn't open + * @since JavaMail 1.5.2 + */ + boolean startIdle(final IdleManager im) throws MessagingException { + // ASSERT: Must NOT be called with this folder's + // synchronization lock held. + assert !Thread.holdsLock(this); + synchronized(this) { + checkOpened(); + if (im != null && idleManager != null && im != idleManager) + throw new MessagingException( + "Folder already being watched by another IdleManager"); + Boolean started = (Boolean)doOptionalCommand("IDLE not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + // if the IdleManager is already watching this folder, + // there's nothing to do here + if (idleState == IDLE && + im != null && im == idleManager) + return Boolean.TRUE; // already watching it + if (idleState == RUNNING) { + p.idleStart(); + logger.finest("startIdle: set to IDLE"); + idleState = IDLE; + idleManager = im; + return Boolean.TRUE; + } else { + // some other thread must be running the IDLE + // command, we'll just wait for it to finish + // without aborting it ourselves + try { + // give up lock and wait to be not idle + messageCacheLock.wait(); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers + // might depend on + Thread.currentThread().interrupt(); + } + return Boolean.FALSE; + } + } + }); + logger.log(Level.FINEST, "startIdle: return {0}", started); + return started.booleanValue(); + } + } + + /** + * Read a response from the server while we're in the IDLE state. + * We hold the messageCacheLock while processing the + * responses so that we can update the number of messages + * in the folder (for example). + * + * @param once only do one notification? + * @return true if we should look for more IDLE responses, + * false if IDLE is done + * @exception MessagingException for errors + * @since JavaMail 1.5.2 + */ + boolean handleIdle(boolean once) throws MessagingException { + Response r = null; + do { + r = protocol.readIdleResponse(); + try { + synchronized (messageCacheLock) { + if (r.isBYE() && r.isSynthetic() && idleState == IDLE) { + /* + * If it was a timeout and no bytes were transferred + * we ignore it and go back and read again. + * If the I/O was otherwise interrupted, and no + * bytes were transferred, we take it as a request + * to abort the IDLE. + */ + Exception ex = r.getException(); + if (ex instanceof InterruptedIOException && + ((InterruptedIOException)ex). + bytesTransferred == 0) { + if (ex instanceof SocketTimeoutException) { + logger.finest( + "handleIdle: ignoring socket timeout"); + r = null; // repeat do/while loop + } else { + logger.finest("handleIdle: interrupting IDLE"); + IdleManager im = idleManager; + if (im != null) { + logger.finest( + "handleIdle: request IdleManager to abort"); + im.requestAbort(this); + } else { + logger.finest("handleIdle: abort IDLE"); + protocol.idleAbort(); + idleState = ABORTING; + } + // normally will exit the do/while loop + } + continue; + } + } + boolean done = true; + try { + if (protocol == null || + !protocol.processIdleResponse(r)) + return false; // done + done = false; + } finally { + if (done) { + logger.finest("handleIdle: set to RUNNING"); + idleState = RUNNING; + idleManager = null; + messageCacheLock.notifyAll(); + } + } + if (once) { + if (idleState == IDLE) { + try { + protocol.idleAbort(); + } catch (Exception ex) { + // ignore any failures, still have to abort. + // connection failures will be detected above + // in the call to readIdleResponse. + } + idleState = ABORTING; + } + } + } + } catch (ConnectionException cex) { + // Oops, the folder died on us. + throw new FolderClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + // keep processing responses already in our buffer + } while (r == null || protocol.hasResponse()); + return true; + } + + /* + * If an IDLE command is in progress, abort it if necessary, + * and wait until it completes. + * ASSERT: Must be called with the message cache lock held. + */ + void waitIfIdle() throws ProtocolException { + assert Thread.holdsLock(messageCacheLock); + while (idleState != RUNNING) { + if (idleState == IDLE) { + IdleManager im = idleManager; + if (im != null) { + logger.finest("waitIfIdle: request IdleManager to abort"); + im.requestAbort(this); + } else { + logger.finest("waitIfIdle: abort IDLE"); + protocol.idleAbort(); + idleState = ABORTING; + } + } else + logger.log(Level.FINEST, "waitIfIdle: idleState {0}", idleState); + try { + // give up lock and wait to be not idle + if (logger.isLoggable(Level.FINEST)) + logger.finest("waitIfIdle: wait to be not idle: " + + Thread.currentThread()); + messageCacheLock.wait(); + if (logger.isLoggable(Level.FINEST)) + logger.finest("waitIfIdle: wait done, idleState " + + idleState + ": " + Thread.currentThread()); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might depend on + Thread.currentThread().interrupt(); + // If someone is trying to interrupt us we can't keep going + // around the loop waiting for IDLE to complete, but we can't + // just return because callers expect the idleState to be + // RUNNING when we return. Throwing this exception seems + // like the best choice. + throw new ProtocolException("Interrupted waitIfIdle", ex); + } + } + } + + /* + * Send the DONE command that aborts the IDLE; used by IdleManager. + */ + void idleAbort() { + synchronized (messageCacheLock) { + if (idleState == IDLE && protocol != null) { + protocol.idleAbort(); + idleState = ABORTING; + } + } + } + + /* + * Send the DONE command that aborts the IDLE and wait for the response; + * used by IdleManager. + */ + void idleAbortWait() { + synchronized (messageCacheLock) { + if (idleState == IDLE && protocol != null) { + protocol.idleAbort(); + idleState = ABORTING; + + // read responses until OK or connection failure + try { + for (;;) { + if (!handleIdle(false)) + break; + } + } catch (Exception ex) { + // assume it's a connection failure; nothing more to do + logger.log(Level.FINEST, "Exception in idleAbortWait", ex); + } + logger.finest("IDLE aborted"); + } + } + } + + /** + * Return the SocketChannel for this connection, if any, for use + * in IdleManager. + */ + SocketChannel getChannel() { + return protocol != null ? protocol.getChannel() : null; + } + + /** + * Send the IMAP ID command (if supported by the server) and return + * the result from the server. The ID command identfies the client + * to the server and returns information about the server to the client. + * See RFC 2971. + * The returned Map is unmodifiable. + * + * @param clientParams a Map of keys and values identifying the client + * @return a Map of keys and values identifying the server + * @exception MessagingException if the server doesn't support the + * ID extension + * @since JavaMail 1.5.1 + */ + @SuppressWarnings("unchecked") + public Map id(final Map clientParams) + throws MessagingException { + checkOpened(); + return (Map)doOptionalCommand("ID not supported", + new ProtocolCommand() { + @Override + public Object doCommand(IMAPProtocol p) + throws ProtocolException { + return p.id(clientParams); + } + }); + } + + /** + * Use the IMAP STATUS command to get the indicated item. + * The STATUS item may be a standard item such as "RECENT" or "UNSEEN", + * or may be a server-specific item. + * The folder must be closed. If the item is not found, or the + * folder is open, -1 is returned. + * + * @param item the STATUS item to fetch + * @return the value of the STATUS item, or -1 + * @exception MessagingException for errors + * @since JavaMail 1.5.2 + */ + public synchronized long getStatusItem(String item) + throws MessagingException { + if (!opened) { + checkExists(); + + IMAPProtocol p = null; + Status status = null; + try { + p = getStoreProtocol(); // XXX + String[] items = { item }; + status = p.status(fullName, items); + return status != null ? status.getItem(item) : -1; + } catch (BadCommandException bex) { + // doesn't support STATUS, probably vanilla IMAP4 .. + // Could EXAMINE, SEARCH for UNREAD messages and + // return the count .. bah, not worth it. + return -1; + } catch (ConnectionException cex) { + throw new StoreClosedException(store, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + return -1; + } + + /** + * The response handler. This is the callback routine that is + * invoked by the protocol layer. + */ + /* + * ASSERT: This method must be called only when holding the + * messageCacheLock. + * ASSERT: This method must *not* invoke any other method that + * might grab the 'folder' lock or 'message' lock (i.e., any + * synchronized methods on IMAPFolder or IMAPMessage) + * since that will result in violating the locking hierarchy. + */ + @Override + public void handleResponse(Response r) { + assert Thread.holdsLock(messageCacheLock); + + /* + * First, delegate possible ALERT or notification to the Store. + */ + if (r.isOK() || r.isNO() || r.isBAD() || r.isBYE()) + ((IMAPStore)store).handleResponseCode(r); + + /* + * Now check whether this is a BYE or OK response and + * handle appropriately. + */ + if (r.isBYE()) { + if (opened) // XXX - accessed without holding folder lock + cleanup(false); + return; + } else if (r.isOK()) { + // HIGHESTMODSEQ can be updated on any OK response + r.skipSpaces(); + if (r.readByte() == '[') { + String s = r.readAtom(); + if (s.equalsIgnoreCase("HIGHESTMODSEQ")) + highestmodseq = r.readLong(); + } + r.reset(); + return; + } else if (!r.isUnTagged()) { + return; // might be a continuation for IDLE + } + + /* Now check whether this is an IMAP specific response */ + if (!(r instanceof IMAPResponse)) { + // Probably a bug in our code ! + // XXX - should be an assert + logger.fine("UNEXPECTED RESPONSE : " + r.toString()); + return; + } + + IMAPResponse ir = (IMAPResponse)r; + + if (ir.keyEquals("EXISTS")) { // EXISTS + int exists = ir.getNumber(); + if (exists <= realTotal) + // Could be the EXISTS following EXPUNGE, ignore 'em + return; + + int count = exists - realTotal; // number of new messages + Message[] msgs = new Message[count]; + + // Add 'count' new IMAPMessage objects into the messageCache + messageCache.addMessages(count, realTotal + 1); + int oldtotal = total; // used in loop below + realTotal += count; + total += count; + + // avoid instantiating Message objects if no listeners. + if (hasMessageCountListener) { + for (int i = 0; i < count; i++) + msgs[i] = messageCache.getMessage(++oldtotal); + + // Notify listeners. + notifyMessageAddedListeners(msgs); + } + + } else if (ir.keyEquals("EXPUNGE")) { + // EXPUNGE response. + + int seqnum = ir.getNumber(); + if (seqnum > realTotal) { + // A message was expunged that we never knew about. + // Exchange will do this. Just ignore the notification. + // (Alternatively, we could simulate an EXISTS for the + // expunged message before expunging it.) + return; + } + Message[] msgs = null; + if (doExpungeNotification && hasMessageCountListener) { + // save the Message object first; can't look it + // up after it's expunged + msgs = new Message[] { getMessageBySeqNumber(seqnum) }; + if (msgs[0] == null) // XXX - should never happen + msgs = null; + } + + messageCache.expungeMessage(seqnum); + + // decrement 'realTotal'; but leave 'total' unchanged + realTotal--; + + if (msgs != null) // Do the notification here. + notifyMessageRemovedListeners(false, msgs); + + } else if (ir.keyEquals("VANISHED")) { + // after the folder is opened with QRESYNC, a VANISHED response + // without the (EARLIER) tag is used instead of the EXPUNGE + // response + + // "VANISHED" SP ["(EARLIER)"] SP known-uids + String[] s = ir.readAtomStringList(); + if (s == null) { // no (EARLIER) + String uids = ir.readAtom(); + UIDSet[] uidset = UIDSet.parseUIDSets(uids); + // assume no duplicates and no UIDs out of range + realTotal -= UIDSet.size(uidset); + long[] luid = UIDSet.toArray(uidset); + Message[] msgs = createMessagesForUIDs(luid); + for (Message m : msgs) { + if (m.getMessageNumber() > 0) + messageCache.expungeMessage(m.getMessageNumber()); + } + if (doExpungeNotification && hasMessageCountListener) { + notifyMessageRemovedListeners(true, msgs); + } + } // else if (EARLIER), ignore + + } else if (ir.keyEquals("FETCH")) { + assert ir instanceof FetchResponse : "!ir instanceof FetchResponse"; + Message msg = processFetchResponse((FetchResponse)ir); + if (msg != null) + notifyMessageChangedListeners( + MessageChangedEvent.FLAGS_CHANGED, msg); + + } else if (ir.keyEquals("RECENT")) { + // update 'recent' + recent = ir.getNumber(); + } + } + + /** + * Process a FETCH response. + * The only unsolicited FETCH response that makes sense + * to me (for now) is FLAGS updates, which might include + * UID and MODSEQ information. Ignore any other junk. + */ + private Message processFetchResponse(FetchResponse fr) { + IMAPMessage msg = getMessageBySeqNumber(fr.getNumber()); + if (msg != null) { // should always be true + boolean notify = false; + + UID uid = fr.getItem(UID.class); + if (uid != null && msg.getUID() != uid.uid) { + msg.setUID(uid.uid); + if (uidTable == null) + uidTable = new Hashtable<>(); + uidTable.put(Long.valueOf(uid.uid), msg); + notify = true; + } + + MODSEQ modseq = fr.getItem(MODSEQ.class); + if (modseq != null && msg._getModSeq() != modseq.modseq) { + msg.setModSeq(modseq.modseq); + /* + * XXX - should we update the folder's HIGHESTMODSEQ or not? + * + if (modseq.modseq > highestmodseq) + highestmodseq = modseq.modseq; + */ + notify = true; + } + + // Get FLAGS response, if present + FLAGS flags = fr.getItem(FLAGS.class); + if (flags != null) { + msg._setFlags(flags); // assume flags changed + notify = true; + } + + // handle any extension items that might've changed + // XXX - no notifications associated with extension items + msg.handleExtensionFetchItems(fr.getExtensionItems()); + + if (!notify) + msg = null; + } + return msg; + } + + /** + * Handle the given array of Responses. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + */ + void handleResponses(Response[] r) { + for (int i = 0; i < r.length; i++) { + if (r[i] != null) + handleResponse(r[i]); + } + } + + /** + * Get this folder's Store's protocol connection. + * + * When acquiring a store protocol object, it is important to + * use the following steps: + * + *

+     *     IMAPProtocol p = null;
+     *     try {
+     *         p = getStoreProtocol();
+     *         // perform the command
+     *     } catch (WhateverException ex) {
+     *         // handle it
+     *     } finally {
+     *         releaseStoreProtocol(p);
+     *     }
+     * 
+ * + * ASSERT: Must be called with this folder's synchronization lock held. + * + * @return the IMAPProtocol for the Store's connection + * @exception ProtocolException for protocol errors + */ + protected synchronized IMAPProtocol getStoreProtocol() + throws ProtocolException { + connectionPoolLogger.fine("getStoreProtocol() borrowing a connection"); + return ((IMAPStore)store).getFolderStoreProtocol(); + } + + /** + * Throw the appropriate 'closed' exception. + * + * @param cex the ConnectionException + * @exception FolderClosedException if the folder is closed + * @exception StoreClosedException if the store is closed + */ + protected synchronized void throwClosedException(ConnectionException cex) + throws FolderClosedException, StoreClosedException { + // If it's the folder's protocol object, throw a FolderClosedException; + // otherwise, throw a StoreClosedException. + // If a command has failed because the connection is closed, + // the folder will have already been forced closed by the + // time we get here and our protocol object will have been + // released, so if we no longer have a protocol object we base + // this decision on whether we *think* the folder is open. + if ((protocol != null && cex.getProtocol() == protocol) || + (protocol == null && !reallyClosed)) + throw new FolderClosedException(this, cex.getMessage()); + else + throw new StoreClosedException(store, cex.getMessage()); + } + + /** + * Return the IMAPProtocol object for this folder.

+ * + * This method will block if necessary to wait for an IDLE + * command to finish. + * + * @return the IMAPProtocol object used when the folder is open + * @exception ProtocolException for protocol errors + */ + protected IMAPProtocol getProtocol() throws ProtocolException { + assert Thread.holdsLock(messageCacheLock); + waitIfIdle(); + // if we no longer have a protocol object after waiting, it probably + // means the connection has been closed due to a communnication error, + // or possibly because the folder has been closed + if (protocol == null) + throw new ConnectionException("Connection closed"); + return protocol; + } + + /** + * A simple interface for user-defined IMAP protocol commands. + */ + public static interface ProtocolCommand { + /** + * Execute the user-defined command using the supplied IMAPProtocol + * object. + * + * @param protocol the IMAPProtocol for the connection + * @return the results of the command + * @exception ProtocolException for protocol errors + */ + public Object doCommand(IMAPProtocol protocol) throws ProtocolException; + } + + /** + * Execute a user-supplied IMAP command. The command is executed + * in the appropriate context with the necessary locks held and + * using the appropriate IMAPProtocol object.

+ * + * This method returns whatever the ProtocolCommand + * object's doCommand method returns. If the + * doCommand method throws a ConnectionException + * it is translated into a StoreClosedException or + * FolderClosedException as appropriate. If the + * doCommand method throws a ProtocolException + * it is translated into a MessagingException.

+ * + * The following example shows how to execute the IMAP NOOP command. + * Executing more complex IMAP commands requires intimate knowledge + * of the com.sun.mail.iap and + * com.sun.mail.imap.protocol packages, best acquired by + * reading the source code. + * + *

+     * import com.sun.mail.iap.*;
+     * import com.sun.mail.imap.*;
+     * import com.sun.mail.imap.protocol.*;
+     *
+     * ...
+     *
+     * IMAPFolder f = (IMAPFolder)folder;
+     * Object val = f.doCommand(new IMAPFolder.ProtocolCommand() {
+     *	public Object doCommand(IMAPProtocol p)
+     *			throws ProtocolException {
+     *	    p.simpleCommand("NOOP", null);
+     *	    return null;
+     *	}
+     * });
+     * 
+ *

+ * + * Here's a more complex example showing how to use the proposed + * IMAP SORT extension: + * + *

+     * import com.sun.mail.iap.*;
+     * import com.sun.mail.imap.*;
+     * import com.sun.mail.imap.protocol.*;
+     *
+     * ...
+     *
+     * IMAPFolder f = (IMAPFolder)folder;
+     * Object val = f.doCommand(new IMAPFolder.ProtocolCommand() {
+     *	public Object doCommand(IMAPProtocol p)
+     *			throws ProtocolException {
+     *	    // Issue command
+     *	    Argument args = new Argument();
+     *	    Argument list = new Argument();
+     *	    list.writeString("SUBJECT");
+     *	    args.writeArgument(list);
+     *	    args.writeString("UTF-8");
+     *	    args.writeString("ALL");
+     *	    Response[] r = p.command("SORT", args);
+     *	    Response response = r[r.length-1];
+     *
+     *	    // Grab response
+     *	    Vector v = new Vector();
+     *	    if (response.isOK()) { // command succesful 
+     *		for (int i = 0, len = r.length; i < len; i++) {
+     *		    if (!(r[i] instanceof IMAPResponse))
+     *			continue;
+     *
+     *		    IMAPResponse ir = (IMAPResponse)r[i];
+     *		    if (ir.keyEquals("SORT")) {
+     *			String num;
+     *			while ((num = ir.readAtomString()) != null)
+     *			    System.out.println(num);
+     *			r[i] = null;
+     *		    }
+     *		}
+     *	    }
+     *
+     *	    // dispatch remaining untagged responses
+     *	    p.notifyResponseHandlers(r);
+     *	    p.handleResult(response);
+     *
+     *	    return null;
+     *	}
+     * });
+     * 
+ * + * @param cmd the protocol command + * @return the result of the command + * @exception MessagingException for failures + */ + public Object doCommand(ProtocolCommand cmd) throws MessagingException { + try { + return doProtocolCommand(cmd); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return null; + } + + public Object doOptionalCommand(String err, ProtocolCommand cmd) + throws MessagingException { + try { + return doProtocolCommand(cmd); + } catch (BadCommandException bex) { + throw new MessagingException(err, bex); + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return null; + } + + public Object doCommandIgnoreFailure(ProtocolCommand cmd) + throws MessagingException { + try { + return doProtocolCommand(cmd); + } catch (CommandFailedException cfx) { + return null; + } catch (ConnectionException cex) { + // Oops, the store or folder died on us. + throwClosedException(cex); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + return null; + } + + protected synchronized Object doProtocolCommand(ProtocolCommand cmd) + throws ProtocolException { + /* + * Check whether we have a protocol object, not whether we're + * opened, to allow use of the exsting protocol object in the + * open method before the state is changed to "opened". + */ + if (protocol != null) { + synchronized (messageCacheLock) { + return cmd.doCommand(getProtocol()); + } + } + + // only get here if using store's connection + IMAPProtocol p = null; + + try { + p = getStoreProtocol(); + return cmd.doCommand(p); + } finally { + releaseStoreProtocol(p); + } + } + + /** + * Release the store protocol object. If we borrowed a protocol + * object from the connection pool, give it back. If we used our + * own protocol object, nothing to do. + * + * ASSERT: Must be called with this folder's synchronization lock held. + * + * @param p the IMAPProtocol object + */ + protected synchronized void releaseStoreProtocol(IMAPProtocol p) { + if (p != protocol) + ((IMAPStore)store).releaseFolderStoreProtocol(p); + else { + // XXX - should never happen + logger.fine("releasing our protocol as store protocol?"); + } + } + + /** + * Release the protocol object. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param returnToPool return the protocol object to the pool? + */ + protected void releaseProtocol(boolean returnToPool) { + if (protocol != null) { + protocol.removeResponseHandler(this); + + if (returnToPool) + ((IMAPStore)store).releaseProtocol(this, protocol); + else { + protocol.disconnect(); // make sure it's disconnected + ((IMAPStore)store).releaseProtocol(this, null); + } + protocol = null; + } + } + + /** + * Issue a noop command for the connection if the connection has not been + * used in more than a second. If keepStoreAlive is true, + * also issue a noop over the store's connection. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param keepStoreAlive keep the Store alive too? + * @exception ProtocolException for protocol errors + */ + protected void keepConnectionAlive(boolean keepStoreAlive) + throws ProtocolException { + + assert Thread.holdsLock(messageCacheLock); + if (protocol == null) // in case connection was closed + return; + if (System.currentTimeMillis() - protocol.getTimestamp() > 1000) { + waitIfIdle(); + if (protocol != null) + protocol.noop(); + } + + if (keepStoreAlive && ((IMAPStore)store).hasSeparateStoreConnection()) { + IMAPProtocol p = null; + try { + p = ((IMAPStore)store).getFolderStoreProtocol(); + if (System.currentTimeMillis() - p.getTimestamp() > 1000) + p.noop(); + } finally { + ((IMAPStore)store).releaseFolderStoreProtocol(p); + } + } + } + + /** + * Get the message object for the given sequence number. If + * none found, null is returned. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param seqnum the message sequence number + * @return the IMAPMessage object + */ + protected IMAPMessage getMessageBySeqNumber(int seqnum) { + if (seqnum > messageCache.size()) { + // Microsoft Exchange will sometimes return message + // numbers that it has not yet notified the client + // about via EXISTS; ignore those messages here. + // GoDaddy IMAP does this too. + if (logger.isLoggable(Level.FINE)) + logger.fine("ignoring message number " + + seqnum + " outside range " + messageCache.size()); + return null; + } + return messageCache.getMessageBySeqnum(seqnum); + } + + /** + * Get the message objects for the given sequence numbers. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock + * + * @param seqnums the array of message sequence numbers + * @return the IMAPMessage objects + * @since JavaMail 1.5.3 + */ + protected IMAPMessage[] getMessagesBySeqNumbers(int[] seqnums) { + IMAPMessage[] msgs = new IMAPMessage[seqnums.length]; + int nulls = 0; + // Map seq-numbers into actual Messages. + for (int i = 0; i < seqnums.length; i++) { + msgs[i] = getMessageBySeqNumber(seqnums[i]); + if (msgs[i] == null) + nulls++; + } + if (nulls > 0) { // compress the array to remove the nulls + IMAPMessage[] nmsgs = new IMAPMessage[seqnums.length - nulls]; + for (int i = 0, j = 0; i < msgs.length; i++) { + if (msgs[i] != null) + nmsgs[j++] = msgs[i]; + } + msgs = nmsgs; + } + return msgs; + } + + private boolean isDirectory() { + return ((type & HOLDS_FOLDERS) != 0); + } +} + +/** + * An object that holds a Message object + * and reports its size and writes it to another OutputStream + * on demand. Used by appendMessages to avoid the need to + * buffer the entire message in memory in a single byte array + * before sending it to the server. + */ +class MessageLiteral implements Literal { + private Message msg; + private int msgSize = -1; + private byte[] buf; // the buffered message, if not null + + public MessageLiteral(Message msg, int maxsize) + throws MessagingException, IOException { + this.msg = msg; + // compute the size here so exceptions can be returned immediately + LengthCounter lc = new LengthCounter(maxsize); + OutputStream os = new CRLFOutputStream(lc); + msg.writeTo(os); + os.flush(); + msgSize = lc.getSize(); + buf = lc.getBytes(); + } + + @Override + public int size() { + return msgSize; + } + + @Override + public void writeTo(OutputStream os) throws IOException { + // the message should not change between the constructor and this call + try { + if (buf != null) + os.write(buf, 0, msgSize); + else { + os = new CRLFOutputStream(os); + msg.writeTo(os); + } + } catch (MessagingException mex) { + // exceptions here are bad, "should" never happen + throw new IOException("MessagingException while appending message: " + + mex); + } + } +} + +/** + * Count the number of bytes written to the stream. + * Also, save a copy of small messages to avoid having to process + * the data again. + */ +class LengthCounter extends OutputStream { + private int size = 0; + private byte[] buf; + private int maxsize; + + public LengthCounter(int maxsize) { + buf = new byte[8192]; + this.maxsize = maxsize; + } + + @Override + public void write(int b) { + int newsize = size + 1; + if (buf != null) { + if (newsize > maxsize && maxsize >= 0) { + buf = null; + } else if (newsize > buf.length) { + byte newbuf[] = new byte[Math.max(buf.length << 1, newsize)]; + System.arraycopy(buf, 0, newbuf, 0, size); + buf = newbuf; + buf[size] = (byte)b; + } else { + buf[size] = (byte)b; + } + } + size = newsize; + } + + @Override + public void write(byte b[], int off, int len) { + if ((off < 0) || (off > b.length) || (len < 0) || + ((off + len) > b.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + int newsize = size + len; + if (buf != null) { + if (newsize > maxsize && maxsize >= 0) { + buf = null; + } else if (newsize > buf.length) { + byte newbuf[] = new byte[Math.max(buf.length << 1, newsize)]; + System.arraycopy(buf, 0, newbuf, 0, size); + buf = newbuf; + System.arraycopy(b, off, buf, size, len); + } else { + System.arraycopy(b, off, buf, size, len); + } + } + size = newsize; + } + + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public int getSize() { + return size; + } + + public byte[] getBytes() { + return buf; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPInputStream.java b/app/src/main/java/com/sun/mail/imap/IMAPInputStream.java new file mode 100644 index 0000000000..52018e288b --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPInputStream.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; +import javax.mail.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.iap.*; +import com.sun.mail.util.FolderClosedIOException; +import com.sun.mail.util.MessageRemovedIOException; + +/** + * This class implements an IMAP data stream. + * + * @author John Mani + */ + +public class IMAPInputStream extends InputStream { + private IMAPMessage msg; // this message + private String section; // section-id + private int pos; // track the position within the IMAP datastream + private int blksize; // number of bytes to read in each FETCH request + private int max; // the total number of bytes in this section. + // -1 indicates unknown + private byte[] buf; // the buffer obtained from fetchBODY() + private int bufcount; // The index one greater than the index of the + // last valid byte in 'buf' + private int bufpos; // The current position within 'buf' + private boolean lastBuffer; // is this the last buffer of data? + private boolean peek; // peek instead of fetch? + private ByteArray readbuf; // reuse for each read + + // Allocate this much extra space in the read buffer to allow + // space for the FETCH response overhead + private static final int slop = 64; + + + /** + * Create an IMAPInputStream. + * + * @param msg the IMAPMessage the data will come from + * @param section the IMAP section/part identifier for the data + * @param max the number of bytes in this section + * @param peek peek instead of fetch? + */ + public IMAPInputStream(IMAPMessage msg, String section, int max, + boolean peek) { + this.msg = msg; + this.section = section; + this.max = max; + this.peek = peek; + pos = 0; + blksize = msg.getFetchBlockSize(); + } + + /** + * Do a NOOP to force any untagged EXPUNGE responses + * and then check if this message is expunged. + */ + private void forceCheckExpunged() + throws MessageRemovedIOException, FolderClosedIOException { + synchronized (msg.getMessageCacheLock()) { + try { + msg.getProtocol().noop(); + } catch (ConnectionException cex) { + throw new FolderClosedIOException(msg.getFolder(), + cex.getMessage()); + } catch (FolderClosedException fex) { + throw new FolderClosedIOException(fex.getFolder(), + fex.getMessage()); + } catch (ProtocolException pex) { + // ignore it + } + } + if (msg.isExpunged()) + throw new MessageRemovedIOException(); + } + + /** + * Fetch more data from the server. This method assumes that all + * data has already been read in, hence bufpos > bufcount. + */ + private void fill() throws IOException { + /* + * If we've read the last buffer, there's no more to read. + * If we know the total number of bytes available from this + * section, let's check if we have consumed that many bytes. + */ + if (lastBuffer || max != -1 && pos >= max) { + if (pos == 0) + checkSeen(); + readbuf = null; // XXX - return to pool? + return; // the caller of fill() will return -1. + } + + BODY b = null; + if (readbuf == null) + readbuf = new ByteArray(blksize + slop); + + ByteArray ba; + int cnt; + // Acquire MessageCacheLock, to freeze seqnum. + synchronized (msg.getMessageCacheLock()) { + try { + IMAPProtocol p = msg.getProtocol(); + + // Check whether this message is expunged + if (msg.isExpunged()) + throw new MessageRemovedIOException( + "No content for expunged message"); + + int seqnum = msg.getSequenceNumber(); + cnt = blksize; + if (max != -1 && pos + blksize > max) + cnt = max - pos; + if (peek) + b = p.peekBody(seqnum, section, pos, cnt, readbuf); + else + b = p.fetchBody(seqnum, section, pos, cnt, readbuf); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new IOException(pex.getMessage()); + } catch (FolderClosedException fex) { + throw new FolderClosedIOException(fex.getFolder(), + fex.getMessage()); + } + + if (b == null || ((ba = b.getByteArray()) == null)) { + forceCheckExpunged(); + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + ba = new ByteArray(0); + } + } + + // make sure the SEEN flag is set after reading the first chunk + if (pos == 0) + checkSeen(); + + // setup new values .. + buf = ba.getBytes(); + bufpos = ba.getStart(); + int n = ba.getCount(); // will be zero, if all data has been + // consumed from the server. + + int origin = b != null ? b.getOrigin() : pos; + if (origin < 0) { + /* + * Some versions of Exchange will return the entire message + * body even though we only ask for a chunk, and the returned + * data won't specify an "origin". If this happens, and we + * get more data than we asked for, assume it's the entire + * message body. + */ + if (pos == 0) { + /* + * If we got more or less than we asked for, + * this is the last buffer of data. + */ + lastBuffer = n != cnt; + } else { + /* + * We asked for data NOT starting at the beginning, + * but we got back data starting at the beginning. + * Possibly we could extract the needed data from + * some part of the data we got back, but really this + * should never happen so we just assume something is + * broken and terminate the data here. + */ + n = 0; + lastBuffer = true; + } + } else if (origin == pos) { + /* + * If we got less than we asked for, + * this is the last buffer of data. + */ + lastBuffer = n < cnt; + } else { + /* + * We got back data that doesn't match the request. + * Just terminate the data here. + */ + n = 0; + lastBuffer = true; + } + + bufcount = bufpos + n; + pos += n; + + } + + /** + * Reads the next byte of data from this buffered input stream. + * If no byte is available, the value -1 is returned. + */ + @Override + public synchronized int read() throws IOException { + if (bufpos >= bufcount) { + fill(); + if (bufpos >= bufcount) + return -1; // EOF + } + return buf[bufpos++] & 0xff; + } + + /** + * Reads up to len bytes of data from this + * input stream into the given buffer.

+ * + * Returns the total number of bytes read into the buffer, + * or -1 if there is no more data.

+ * + * Note that this method mimics the "weird !" semantics of + * BufferedInputStream in that the number of bytes actually + * returned may be less that the requested value. So callers + * of this routine should be aware of this and must check + * the return value to insure that they have obtained the + * requisite number of bytes. + */ + @Override + public synchronized int read(byte b[], int off, int len) + throws IOException { + + int avail = bufcount - bufpos; + if (avail <= 0) { + fill(); + avail = bufcount - bufpos; + if (avail <= 0) + return -1; // EOF + } + int cnt = (avail < len) ? avail : len; + System.arraycopy(buf, bufpos, b, off, cnt); + bufpos += cnt; + return cnt; + } + + /** + * Reads up to b.length bytes of data from this input + * stream into an array of bytes.

+ * + * Returns the total number of bytes read into the buffer, or + * -1 is there is no more data.

+ * + * Note that this method mimics the "weird !" semantics of + * BufferedInputStream in that the number of bytes actually + * returned may be less that the requested value. So callers + * of this routine should be aware of this and must check + * the return value to insure that they have obtained the + * requisite number of bytes. + */ + @Override + public int read(byte b[]) throws IOException { + return read(b, 0, b.length); + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. + */ + @Override + public synchronized int available() throws IOException { + return (bufcount - bufpos); + } + + /** + * Normally the SEEN flag will have been set by now, but if not, + * force it to be set (as long as the folder isn't open read-only + * and we're not peeking). + * And of course, if there's no folder (e.g., a nested message) + * don't do anything. + */ + private void checkSeen() { + if (peek) // if we're peeking, don't set the SEEN flag + return; + try { + Folder f = msg.getFolder(); + if (f != null && f.getMode() != Folder.READ_ONLY && + !msg.isSet(Flags.Flag.SEEN)) + msg.setFlag(Flags.Flag.SEEN, true); + } catch (MessagingException ex) { + // ignore it + } + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPMessage.java b/app/src/main/java/com/sun/mail/imap/IMAPMessage.java new file mode 100644 index 0000000000..453479d989 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPMessage.java @@ -0,0 +1,1700 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import java.io.*; +import java.util.*; + +import javax.mail.*; +import javax.mail.internet.*; +import javax.activation.*; + +import com.sun.mail.util.ReadableMime; +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; + +/** + * This class implements an IMAPMessage object.

+ * + * An IMAPMessage object starts out as a light-weight object. It gets + * filled-in incrementally when a request is made for some item. Or + * when a prefetch is done using the FetchProfile.

+ * + * An IMAPMessage has a messageNumber and a sequenceNumber. The + * messageNumber is its index into its containing folder's messageCache. + * The sequenceNumber is its IMAP sequence-number. + * + * @author John Mani + * @author Bill Shannon + */ +/* + * The lock hierarchy is that the lock on the IMAPMessage object, if + * it's acquired at all, must be acquired before the message cache lock. + * The IMAPMessage lock protects the message flags, sort of. + * + * XXX - I'm not convinced that all fields of IMAPMessage are properly + * protected by locks. + */ + +public class IMAPMessage extends MimeMessage implements ReadableMime { + protected BODYSTRUCTURE bs; // BODYSTRUCTURE + protected ENVELOPE envelope; // ENVELOPE + + /** + * A map of the extension FETCH items. In addition to saving the + * data in this map, an entry in this map indicates that we *have* + * the data, and so it doesn't need to be fetched again. The map + * is created only when needed, to avoid significantly increasing + * the effective size of an IMAPMessage object. + * + * @since JavaMail 1.4.6 + */ + protected Map items; // Map + + private Date receivedDate; // INTERNALDATE + private long size = -1; // RFC822.SIZE + + private Boolean peek; // use BODY.PEEK when fetching content? + + // this message's IMAP UID + private volatile long uid = -1; + + // this message's IMAP MODSEQ - RFC 4551 CONDSTORE + private volatile long modseq = -1; + + // this message's IMAP sectionId (null for toplevel message, + // non-null for a nested message) + protected String sectionId; + + // processed values + private String type; // Content-Type (with params) + private String subject; // decoded (Unicode) subject + private String description; // decoded (Unicode) desc + + // Indicates that we've loaded *all* headers for this message + private volatile boolean headersLoaded = false; + + // Indicates that we've cached the body of this message + private volatile boolean bodyLoaded = false; + + /* Hashtable of names of headers we've loaded from the server. + * Used in isHeaderLoaded() and getHeaderLoaded() to keep track + * of those headers we've attempted to load from the server. We + * need this table of names to avoid multiple attempts at loading + * headers that don't exist for a particular message. + * + * Could this somehow be included in the InternetHeaders object ?? + */ + private Hashtable loadedHeaders + = new Hashtable<>(1); + + // This is our Envelope + static final String EnvelopeCmd = "ENVELOPE INTERNALDATE RFC822.SIZE"; + + /** + * Constructor. + * + * @param folder the folder containing this message + * @param msgnum the message sequence number + */ + protected IMAPMessage(IMAPFolder folder, int msgnum) { + super(folder, msgnum); + flags = null; + } + + /** + * Constructor, for use by IMAPNestedMessage. + * + * @param session the Session + */ + protected IMAPMessage(Session session) { + super(session); + } + + /** + * Get this message's folder's protocol connection. + * Throws FolderClosedException, if the protocol connection + * is not available. + * + * ASSERT: Must hold the messageCacheLock. + * + * @return the IMAPProtocol object for the containing folder + * @exception ProtocolException for protocol errors + * @exception FolderClosedException if the folder is closed + */ + protected IMAPProtocol getProtocol() + throws ProtocolException, FolderClosedException { + ((IMAPFolder)folder).waitIfIdle(); + IMAPProtocol p = ((IMAPFolder)folder).protocol; + if (p == null) + throw new FolderClosedException(folder); + else + return p; + } + + /* + * Is this an IMAP4 REV1 server? + */ + protected boolean isREV1() throws FolderClosedException { + // access the folder's protocol object without waiting + // for IDLE to complete + IMAPProtocol p = ((IMAPFolder)folder).protocol; + if (p == null) + throw new FolderClosedException(folder); + else + return p.isREV1(); + } + + /** + * Get the messageCacheLock, associated with this Message's + * Folder. + * + * @return the message cache lock object + */ + protected Object getMessageCacheLock() { + return ((IMAPFolder)folder).messageCacheLock; + } + + /** + * Get this message's IMAP sequence number. + * + * ASSERT: This method must be called only when holding the + * messageCacheLock. + * + * @return the message sequence number + */ + protected int getSequenceNumber() { + return ((IMAPFolder)folder).messageCache.seqnumOf(getMessageNumber()); + } + + /** + * Wrapper around the protected method Message.setMessageNumber() to + * make that method accessible to IMAPFolder. + */ + @Override + protected void setMessageNumber(int msgnum) { + super.setMessageNumber(msgnum); + } + + /** + * Return the UID for this message. + * Returns -1 if not known; use UIDFolder.getUID() in this case. + * + * @return the UID + * @see javax.mail.UIDFolder#getUID + */ + protected long getUID() { + return uid; + } + + protected void setUID(long uid) { + this.uid = uid; + } + + /** + * Return the modification sequence number (MODSEQ) for this message. + * Returns -1 if not known. + * + * @return the modification sequence number + * @exception MessagingException for failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public synchronized long getModSeq() throws MessagingException { + if (modseq != -1) + return modseq; + + synchronized (getMessageCacheLock()) { // Acquire Lock + try { + IMAPProtocol p = getProtocol(); + checkExpunged(); // insure that message is not expunged + MODSEQ ms = p.fetchMODSEQ(getSequenceNumber()); + + if (ms != null) + modseq = ms.modseq; + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + return modseq; + } + + long _getModSeq() { + return modseq; + } + + void setModSeq(long modseq) { + this.modseq = modseq; + } + + // expose to MessageCache + @Override + protected void setExpunged(boolean set) { + super.setExpunged(set); + } + + // Convenience routine + protected void checkExpunged() throws MessageRemovedException { + if (expunged) + throw new MessageRemovedException(); + } + + /** + * Do a NOOP to force any untagged EXPUNGE responses + * and then check if this message is expunged. + * + * @exception MessageRemovedException if the message has been removed + * @exception FolderClosedException if the folder has been closed + */ + protected void forceCheckExpunged() + throws MessageRemovedException, FolderClosedException { + synchronized (getMessageCacheLock()) { + try { + getProtocol().noop(); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + // ignore it + } + } + if (expunged) + throw new MessageRemovedException(); + } + + // Return the block size for FETCH requests + // MUST be overridden by IMAPNestedMessage + protected int getFetchBlockSize() { + return ((IMAPStore)folder.getStore()).getFetchBlockSize(); + } + + // Should we ignore the size in the BODYSTRUCTURE? + // MUST be overridden by IMAPNestedMessage + protected boolean ignoreBodyStructureSize() { + return ((IMAPStore)folder.getStore()).ignoreBodyStructureSize(); + } + + /** + * Get the "From" attribute. + */ + @Override + public Address[] getFrom() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getFrom(); + loadEnvelope(); + InternetAddress[] a = envelope.from; + /* + * Per RFC 2822, the From header is required, and thus the IMAP + * spec also requires that it be present, but we know that in + * practice it is often missing. Some servers fill in the + * From field with the Sender field in this case, but at least + * Exchange 2007 does not. Use the same fallback strategy used + * by MimeMessage. + */ + if (a == null || a.length == 0) + a = envelope.sender; + return aaclone(a); + } + + @Override + public void setFrom(Address address) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void addFrom(Address[] addresses) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the "Sender" attribute. + */ + @Override + public Address getSender() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getSender(); + loadEnvelope(); + if (envelope.sender != null && envelope.sender.length > 0) + return (envelope.sender)[0]; // there can be only one sender + else + return null; + } + + + @Override + public void setSender(Address address) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the desired Recipient type. + */ + @Override + public Address[] getRecipients(Message.RecipientType type) + throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getRecipients(type); + loadEnvelope(); + + if (type == Message.RecipientType.TO) + return aaclone(envelope.to); + else if (type == Message.RecipientType.CC) + return aaclone(envelope.cc); + else if (type == Message.RecipientType.BCC) + return aaclone(envelope.bcc); + else + return super.getRecipients(type); + } + + @Override + public void setRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void addRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the ReplyTo addresses. + */ + @Override + public Address[] getReplyTo() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getReplyTo(); + loadEnvelope(); + /* + * The IMAP spec requires that the Reply-To field never be + * null, but at least Exchange 2007 fails to fill it in in + * some cases. Use the same fallback strategy used by + * MimeMessage. + */ + if (envelope.replyTo == null || envelope.replyTo.length == 0) + return getFrom(); + return aaclone(envelope.replyTo); + } + + @Override + public void setReplyTo(Address[] addresses) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the decoded subject. + */ + @Override + public String getSubject() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getSubject(); + + if (subject != null) // already cached ? + return subject; + + loadEnvelope(); + if (envelope.subject == null) // no subject + return null; + + // Cache and return the decoded value. + try { + // The server *should* unfold the value, but just in case it + // doesn't we unfold it here. + subject = + MimeUtility.decodeText(MimeUtility.unfold(envelope.subject)); + } catch (UnsupportedEncodingException ex) { + subject = envelope.subject; + } + + return subject; + } + + @Override + public void setSubject(String subject, String charset) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the SentDate. + */ + @Override + public Date getSentDate() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getSentDate(); + loadEnvelope(); + if (envelope.date == null) + return null; + else + return new Date(envelope.date.getTime()); + } + + @Override + public void setSentDate(Date d) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the received date (INTERNALDATE). + */ + @Override + public Date getReceivedDate() throws MessagingException { + checkExpunged(); + if (receivedDate == null) + loadEnvelope(); // have to go to the server for this + if (receivedDate == null) + return null; + else + return new Date(receivedDate.getTime()); + } + + /** + * Get the message size.

+ * + * Note that this returns RFC822.SIZE. That is, it's the + * size of the whole message, header and body included. + * Note also that if the size of the message is greater than + * Integer.MAX_VALUE (2GB), this method returns Integer.MAX_VALUE. + */ + @Override + public int getSize() throws MessagingException { + checkExpunged(); + // if bodyLoaded, size is already set + if (size == -1) + loadEnvelope(); // XXX - could just fetch the size + if (size > Integer.MAX_VALUE) + return Integer.MAX_VALUE; // the best we can do... + else + return (int)size; + } + + /** + * Get the message size as a long.

+ * + * Suitable for messages that might be larger than 2GB. + * @return the message size as a long integer + * @exception MessagingException for failures + * @since JavaMail 1.6 + */ + public long getSizeLong() throws MessagingException { + checkExpunged(); + // if bodyLoaded, size is already set + if (size == -1) + loadEnvelope(); // XXX - could just fetch the size + return size; + } + + /** + * Get the total number of lines.

+ * + * Returns the "body_fld_lines" field from the + * BODYSTRUCTURE. Note that this field is available + * only for text/plain and message/rfc822 types + */ + @Override + public int getLineCount() throws MessagingException { + checkExpunged(); + // XXX - superclass doesn't implement this + loadBODYSTRUCTURE(); + return bs.lines; + } + + /** + * Get the content language. + */ + @Override + public String[] getContentLanguage() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentLanguage(); + loadBODYSTRUCTURE(); + if (bs.language != null) + return bs.language.clone(); + else + return null; + } + + @Override + public void setContentLanguage(String[] languages) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the In-Reply-To header. + * + * @return the In-Reply-To header + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public String getInReplyTo() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getHeader("In-Reply-To", " "); + loadEnvelope(); + return envelope.inReplyTo; + } + + /** + * Get the Content-Type. + * + * Generate this header from the BODYSTRUCTURE. Append parameters + * as well. + */ + @Override + public synchronized String getContentType() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentType(); + + // If we haven't cached the type yet .. + if (type == null) { + loadBODYSTRUCTURE(); + // generate content-type from BODYSTRUCTURE + ContentType ct = new ContentType(bs.type, bs.subtype, bs.cParams); + type = ct.toString(); + } + return type; + } + + /** + * Get the Content-Disposition. + */ + @Override + public String getDisposition() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getDisposition(); + loadBODYSTRUCTURE(); + return bs.disposition; + } + + @Override + public void setDisposition(String disposition) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the Content-Transfer-Encoding. + */ + @Override + public String getEncoding() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getEncoding(); + loadBODYSTRUCTURE(); + return bs.encoding; + } + + /** + * Get the Content-ID. + */ + @Override + public String getContentID() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentID(); + loadBODYSTRUCTURE(); + return bs.id; + } + + @Override + public void setContentID(String cid) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the Content-MD5. + */ + @Override + public String getContentMD5() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getContentMD5(); + loadBODYSTRUCTURE(); + return bs.md5; + } + + @Override + public void setContentMD5(String md5) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the decoded Content-Description. + */ + @Override + public String getDescription() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getDescription(); + + if (description != null) // cached value ? + return description; + + loadBODYSTRUCTURE(); + if (bs.description == null) + return null; + + try { + description = MimeUtility.decodeText(bs.description); + } catch (UnsupportedEncodingException ex) { + description = bs.description; + } + + return description; + } + + @Override + public void setDescription(String description, String charset) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get the Message-ID. + */ + @Override + public String getMessageID() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getMessageID(); + loadEnvelope(); + return envelope.messageId; + } + + /** + * Get the "filename" Disposition parameter. (Only available in + * IMAP4rev1). If thats not available, get the "name" ContentType + * parameter. + */ + @Override + public String getFileName() throws MessagingException { + checkExpunged(); + if (bodyLoaded) + return super.getFileName(); + + String filename = null; + loadBODYSTRUCTURE(); + + if (bs.dParams != null) + filename = bs.dParams.get("filename"); + if (filename == null && bs.cParams != null) + filename = bs.cParams.get("name"); + return filename; + } + + @Override + public void setFileName(String filename) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get all the bytes for this message. Overrides getContentStream() + * in MimeMessage. This method is ultimately used by the DataHandler + * to obtain the input stream for this message. + * + * @see javax.mail.internet.MimeMessage#getContentStream + */ + @Override + protected InputStream getContentStream() throws MessagingException { + if (bodyLoaded) + return super.getContentStream(); + InputStream is = null; + boolean pk = getPeek(); // get before acquiring message cache lock + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + if (p.isREV1() && (getFetchBlockSize() != -1)) // IMAP4rev1 + return new IMAPInputStream(this, toSection("TEXT"), + bs != null && !ignoreBodyStructureSize() ? + bs.size : -1, pk); + + if (p.isREV1()) { + BODY b; + if (pk) + b = p.peekBody(getSequenceNumber(), toSection("TEXT")); + else + b = p.fetchBody(getSequenceNumber(), toSection("TEXT")); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), "TEXT"); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } + + if (is == null) { + forceCheckExpunged(); // may throw MessageRemovedException + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + is = new ByteArrayInputStream(new byte[0]); + } + return is; + } + + /** + * Get the DataHandler object for this message. + */ + @Override + public synchronized DataHandler getDataHandler() + throws MessagingException { + checkExpunged(); + + if (dh == null && !bodyLoaded) { + loadBODYSTRUCTURE(); + if (type == null) { // type not yet computed + // generate content-type from BODYSTRUCTURE + ContentType ct = new ContentType(bs.type, bs.subtype, + bs.cParams); + type = ct.toString(); + } + + /* Special-case Multipart and Nested content. All other + * cases are handled by the superclass. + */ + if (bs.isMulti()) + dh = new DataHandler( + new IMAPMultipartDataSource(this, bs.bodies, + sectionId, this) + ); + else if (bs.isNested() && isREV1() && bs.envelope != null) + /* Nested messages are handled specially only for + * IMAP4rev1. IMAP4 doesn't provide enough support to + * FETCH the components of nested messages + */ + dh = new DataHandler( + new IMAPNestedMessage(this, + bs.bodies[0], + bs.envelope, + sectionId == null ? "1" : sectionId + ".1"), + type + ); + } + + return super.getDataHandler(); + } + + @Override + public void setDataHandler(DataHandler content) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Return the MIME format stream corresponding to this message. + * + * @return the MIME format stream + * @since JavaMail 1.4.5 + */ + @Override + public InputStream getMimeStream() throws MessagingException { + // XXX - need an "if (bodyLoaded)" version + InputStream is = null; + boolean pk = getPeek(); // get before acquiring message cache lock + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + checkExpunged(); // insure this message is not expunged + + if (p.isREV1() && (getFetchBlockSize() != -1)) // IMAP4rev1 + return new IMAPInputStream(this, sectionId, -1, pk); + + if (p.isREV1()) { + BODY b; + if (pk) + b = p.peekBody(getSequenceNumber(), sectionId); + else + b = p.fetchBody(getSequenceNumber(), sectionId); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), null); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } + + if (is == null) { + forceCheckExpunged(); // may throw MessageRemovedException + // nope, the server doesn't think it's expunged. + // can't tell the difference between the server returning NIL + // and some other error that caused null to be returned above, + // so we'll just assume it was empty content. + is = new ByteArrayInputStream(new byte[0]); + } + return is; + } + + /** + * Write out the bytes into the given OutputStream. + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + if (bodyLoaded) { + super.writeTo(os); + return; + } + InputStream is = getMimeStream(); + try { + // write out the bytes + byte[] bytes = new byte[16*1024]; + int count; + while ((count = is.read(bytes)) != -1) + os.write(bytes, 0, count); + } finally { + is.close(); + } + } + + /** + * Get the named header. + */ + @Override + public String[] getHeader(String name) throws MessagingException { + checkExpunged(); + + if (isHeaderLoaded(name)) // already loaded ? + return headers.getHeader(name); + + // Load this particular header + InputStream is = null; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + if (p.isREV1()) { + BODY b = p.peekBody(getSequenceNumber(), + toSection("HEADER.FIELDS (" + name + ")") + ); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), + "HEADER.LINES (" + name + ")"); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } + + // if we get this far without "is" being set, something has gone + // wrong; prevent a later NullPointerException and return null here + if (is == null) + return null; + + if (headers == null) + headers = new InternetHeaders(); + headers.load(is); // load this header into the Headers object. + setHeaderLoaded(name); // Mark this header as loaded + + return headers.getHeader(name); + } + + /** + * Get the named header. + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + checkExpunged(); + + // force the header to be loaded by invoking getHeader(name) + if (getHeader(name) == null) + return null; + return headers.getHeader(name, delimiter); + } + + @Override + public void setHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void addHeader(String name, String value) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + @Override + public void removeHeader(String name) + throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get all headers. + */ + @Override + public Enumeration

getAllHeaders() throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getAllHeaders(); + } + + /** + * Get matching headers. + */ + @Override + public Enumeration
getMatchingHeaders(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getMatchingHeaders(names); + } + + /** + * Get non-matching headers. + */ + @Override + public Enumeration
getNonMatchingHeaders(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getNonMatchingHeaders(names); + } + + @Override + public void addHeaderLine(String line) throws MessagingException { + throw new IllegalWriteException("IMAPMessage is read-only"); + } + + /** + * Get all header-lines. + */ + @Override + public Enumeration getAllHeaderLines() throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getAllHeaderLines(); + } + + /** + * Get all matching header-lines. + */ + @Override + public Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getMatchingHeaderLines(names); + } + + /** + * Get all non-matching headerlines. + */ + @Override + public Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException { + checkExpunged(); + loadHeaders(); + return super.getNonMatchingHeaderLines(names); + } + + /** + * Get the Flags for this message. + */ + @Override + public synchronized Flags getFlags() throws MessagingException { + checkExpunged(); + loadFlags(); + return super.getFlags(); + } + + /** + * Test if the given Flags are set in this message. + */ + @Override + public synchronized boolean isSet(Flags.Flag flag) + throws MessagingException { + checkExpunged(); + loadFlags(); + return super.isSet(flag); + } + + /** + * Set/Unset the given flags in this message. + */ + @Override + public synchronized void setFlags(Flags flag, boolean set) + throws MessagingException { + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + checkExpunged(); // Insure that this message is not expunged + p.storeFlags(getSequenceNumber(), flag, set); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } + } + } + + /** + * Set whether or not to use the PEEK variant of FETCH when + * fetching message content. This overrides the default + * value from the "mail.imap.peek" property. + * + * @param peek the peek flag + * @since JavaMail 1.3.3 + */ + public synchronized void setPeek(boolean peek) { + this.peek = Boolean.valueOf(peek); + } + + /** + * Get whether or not to use the PEEK variant of FETCH when + * fetching message content. + * + * @return the peek flag + * @since JavaMail 1.3.3 + */ + public synchronized boolean getPeek() { + if (peek == null) + return ((IMAPStore)folder.getStore()).getPeek(); + else + return peek.booleanValue(); + } + + /** + * Invalidate cached header and envelope information for this + * message. Subsequent accesses of this information will + * cause it to be fetched from the server. + * + * @since JavaMail 1.3.3 + */ + public synchronized void invalidateHeaders() { + headersLoaded = false; + loadedHeaders.clear(); + headers = null; + envelope = null; + bs = null; + receivedDate = null; + size = -1; + type = null; + subject = null; + description = null; + flags = null; + content = null; + contentStream = null; + bodyLoaded = false; + } + + /** + * This class implements the test to be done on each + * message in the folder. The test is to check whether the + * message has already cached all the items requested in the + * FetchProfile. If any item is missing, the test succeeds and + * breaks out. + */ + public static class FetchProfileCondition implements Utility.Condition { + private boolean needEnvelope = false; + private boolean needFlags = false; + private boolean needBodyStructure = false; + private boolean needUID = false; + private boolean needHeaders = false; + private boolean needSize = false; + private boolean needMessage = false; + private boolean needRDate = false; + private String[] hdrs = null; + private Set need = new HashSet<>(); + + /** + * Create a FetchProfileCondition to determine if we need to fetch + * any of the information specified in the FetchProfile. + * + * @param fp the FetchProfile + * @param fitems the FETCH items + */ + @SuppressWarnings("deprecation") // for FetchProfile.Item.SIZE + public FetchProfileCondition(FetchProfile fp, FetchItem[] fitems) { + if (fp.contains(FetchProfile.Item.ENVELOPE)) + needEnvelope = true; + if (fp.contains(FetchProfile.Item.FLAGS)) + needFlags = true; + if (fp.contains(FetchProfile.Item.CONTENT_INFO)) + needBodyStructure = true; + if (fp.contains(FetchProfile.Item.SIZE)) + needSize = true; + if (fp.contains(UIDFolder.FetchProfileItem.UID)) + needUID = true; + if (fp.contains(IMAPFolder.FetchProfileItem.HEADERS)) + needHeaders = true; + if (fp.contains(IMAPFolder.FetchProfileItem.SIZE)) + needSize = true; + if (fp.contains(IMAPFolder.FetchProfileItem.MESSAGE)) + needMessage = true; + if (fp.contains(IMAPFolder.FetchProfileItem.INTERNALDATE)) + needRDate = true; + hdrs = fp.getHeaderNames(); + for (int i = 0; i < fitems.length; i++) { + if (fp.contains(fitems[i].getFetchProfileItem())) + need.add(fitems[i]); + } + } + + /** + * Return true if we NEED to fetch the requested information + * for the specified message. + */ + @Override + public boolean test(IMAPMessage m) { + if (needEnvelope && m._getEnvelope() == null && !m.bodyLoaded) + return true; // no envelope + if (needFlags && m._getFlags() == null) + return true; // no flags + if (needBodyStructure && m._getBodyStructure() == null && + !m.bodyLoaded) + return true; // no BODYSTRUCTURE + if (needUID && m.getUID() == -1) // no UID + return true; + if (needHeaders && !m.areHeadersLoaded()) // no headers + return true; + if (needSize && m.size == -1 && !m.bodyLoaded) // no size + return true; + if (needMessage && !m.bodyLoaded) // no message body + return true; + if (needRDate && m.receivedDate == null) // no received date + return true; + + // Is the desired header present ? + for (int i = 0; i < hdrs.length; i++) { + if (!m.isHeaderLoaded(hdrs[i])) + return true; // Nope, return + } + Iterator it = need.iterator(); + while (it.hasNext()) { + FetchItem fitem = it.next(); + if (m.items == null || m.items.get(fitem.getName()) == null) + return true; + } + + return false; + } + } + + /** + * Apply the data in the FETCH item to this message. + * + * ASSERT: Must hold the messageCacheLock. + * + * @param item the fetch item + * @param hdrs the headers we're asking for + * @param allHeaders load all headers? + * @return did we handle this fetch item? + * @exception MessagingException for failures + * @since JavaMail 1.4.6 + */ + protected boolean handleFetchItem(Item item, + String[] hdrs, boolean allHeaders) + throws MessagingException { + // Check for the FLAGS item + if (item instanceof Flags) + flags = (Flags)item; + // Check for ENVELOPE items + else if (item instanceof ENVELOPE) + envelope = (ENVELOPE)item; + else if (item instanceof INTERNALDATE) + receivedDate = ((INTERNALDATE)item).getDate(); + else if (item instanceof RFC822SIZE) + size = ((RFC822SIZE)item).size; + else if (item instanceof MODSEQ) + modseq = ((MODSEQ)item).modseq; + + // Check for the BODYSTRUCTURE item + else if (item instanceof BODYSTRUCTURE) + bs = (BODYSTRUCTURE)item; + // Check for the UID item + else if (item instanceof UID) { + UID u = (UID)item; + uid = u.uid; // set uid + // add entry into uid table + if (((IMAPFolder)folder).uidTable == null) + ((IMAPFolder) folder).uidTable + = new Hashtable<>(); + ((IMAPFolder)folder).uidTable.put(Long.valueOf(u.uid), this); + } + + // Check for header items + else if (item instanceof RFC822DATA || + item instanceof BODY) { + InputStream headerStream; + boolean isHeader; + if (item instanceof RFC822DATA) { // IMAP4 + headerStream = + ((RFC822DATA)item).getByteArrayInputStream(); + isHeader = ((RFC822DATA)item).isHeader(); + } else { // IMAP4rev1 + headerStream = + ((BODY)item).getByteArrayInputStream(); + isHeader = ((BODY)item).isHeader(); + } + + if (!isHeader) { + // load the entire message by using the superclass + // MimeMessage.parse method + // first, save the size of the message + try { + size = headerStream.available(); + } catch (IOException ex) { + // should never occur + } + parse(headerStream); + bodyLoaded = true; + setHeadersLoaded(true); + } else { + // Load the obtained headers. + InternetHeaders h = new InternetHeaders(); + // Some IMAP servers (e.g., gmx.net) return NIL + // instead of a string just containing a CR/LF + // when the header list is empty. + if (headerStream != null) + h.load(headerStream); + if (headers == null || allHeaders) + headers = h; + else { + /* + * This is really painful. A second fetch + * of the same headers (which might occur because + * a new header was added to the set requested) + * will return headers we already know about. + * In this case, only load the headers we haven't + * seen before to avoid adding duplicates of + * headers we already have. + * + * XXX - There's a race condition here if another + * thread is reading headers in the same message + * object, because InternetHeaders is not thread + * safe. + */ + Enumeration
e = h.getAllHeaders(); + while (e.hasMoreElements()) { + Header he = e.nextElement(); + if (!isHeaderLoaded(he.getName())) + headers.addHeader( + he.getName(), he.getValue()); + } + } + + // if we asked for all headers, assume we got them + if (allHeaders) + setHeadersLoaded(true); + else { + // Mark all headers we asked for as 'loaded' + for (int k = 0; k < hdrs.length; k++) + setHeaderLoaded(hdrs[k]); + } + } + } else + return false; // not handled + return true; // something above handled it + } + + /** + * Apply the data in the extension FETCH items to this message. + * This method adds all the items to the items map. + * Subclasses may override this method to call super and then + * also copy the data to a more convenient form. + * + * ASSERT: Must hold the messageCacheLock. + * + * @param extensionItems the Map to add fetch items to + * @since JavaMail 1.4.6 + */ + protected void handleExtensionFetchItems( + Map extensionItems) { + if (extensionItems == null || extensionItems.isEmpty()) + return; + if (items == null) + items = new HashMap<>(); + items.putAll(extensionItems); + } + + /** + * Fetch an individual item for the current message. + * Note that handleExtensionFetchItems will have been called + * to store this item in the message before this method + * returns. + * + * @param fitem the FetchItem + * @return the data associated with the FetchItem + * @exception MessagingException for failures + * @since JavaMail 1.4.6 + */ + protected Object fetchItem(FetchItem fitem) + throws MessagingException { + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + Object robj = null; + + try { + IMAPProtocol p = getProtocol(); + + checkExpunged(); // Insure that this message is not expunged + + int seqnum = getSequenceNumber(); + Response[] r = p.fetch(seqnum, fitem.getName()); + + for (int i = 0; i < r.length; i++) { + // If this response is NOT a FetchResponse or if it does + // not match our seqnum, skip. + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != seqnum) + continue; + + FetchResponse f = (FetchResponse)r[i]; + handleExtensionFetchItems(f.getExtensionItems()); + if (items != null) { + Object o = items.get(fitem.getName()); + if (o != null) + robj = o; + } + } + + // ((IMAPFolder)folder).handleResponses(r); + p.notifyResponseHandlers(r); + p.handleResult(r[r.length - 1]); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + return robj; + + } // Release MessageCacheLock + } + + /** + * Return the data associated with the FetchItem. + * If the data hasn't been fetched, call the fetchItem + * method to fetch it. Returns null if there is no + * data for the FetchItem. + * + * @param fitem the FetchItem + * @return the data associated with the FetchItem + * @exception MessagingException for failures + * @since JavaMail 1.4.6 + */ + public synchronized Object getItem(FetchItem fitem) + throws MessagingException { + Object item = items == null ? null : items.get(fitem.getName()); + if (item == null) + item = fetchItem(fitem); + return item; + } + + /* + * Load the Envelope for this message. + */ + private synchronized void loadEnvelope() throws MessagingException { + if (envelope != null) // already loaded + return; + + Response[] r = null; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + checkExpunged(); // Insure that this message is not expunged + + int seqnum = getSequenceNumber(); + r = p.fetch(seqnum, EnvelopeCmd); + + for (int i = 0; i < r.length; i++) { + // If this response is NOT a FetchResponse or if it does + // not match our seqnum, skip. + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != seqnum) + continue; + + FetchResponse f = (FetchResponse)r[i]; + + // Look for the Envelope items. + int count = f.getItemCount(); + for (int j = 0; j < count; j++) { + Item item = f.getItem(j); + + if (item instanceof ENVELOPE) + envelope = (ENVELOPE)item; + else if (item instanceof INTERNALDATE) + receivedDate = ((INTERNALDATE)item).getDate(); + else if (item instanceof RFC822SIZE) + size = ((RFC822SIZE)item).size; + } + } + + // ((IMAPFolder)folder).handleResponses(r); + p.notifyResponseHandlers(r); + p.handleResult(r[r.length - 1]); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + + } // Release MessageCacheLock + + if (envelope == null) + throw new MessagingException("Failed to load IMAP envelope"); + } + + /* + * Load the BODYSTRUCTURE + */ + private synchronized void loadBODYSTRUCTURE() + throws MessagingException { + if (bs != null) // already loaded + return; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + bs = p.fetchBodyStructure(getSequenceNumber()); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + if (bs == null) { + // if the FETCH is successful, we should always get a + // BODYSTRUCTURE, but some servers fail to return it + // if the message has been expunged + forceCheckExpunged(); + throw new MessagingException("Unable to load BODYSTRUCTURE"); + } + } + } + + /* + * Load all headers. + */ + private synchronized void loadHeaders() throws MessagingException { + if (headersLoaded) + return; + + InputStream is = null; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized (getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + if (p.isREV1()) { + BODY b = p.peekBody(getSequenceNumber(), + toSection("HEADER")); + if (b != null) + is = b.getByteArrayInputStream(); + } else { + RFC822DATA rd = p.fetchRFC822(getSequenceNumber(), + "HEADER"); + if (rd != null) + is = rd.getByteArrayInputStream(); + } + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } // Release MessageCacheLock + + if (is == null) + throw new MessagingException("Cannot load header"); + headers = new InternetHeaders(is); + headersLoaded = true; + } + + /* + * Load this message's Flags + */ + private synchronized void loadFlags() throws MessagingException { + if (flags != null) + return; + + // Acquire MessageCacheLock, to freeze seqnum. + synchronized(getMessageCacheLock()) { + try { + IMAPProtocol p = getProtocol(); + + // This message could be expunged when we were waiting + // to acquire the lock ... + checkExpunged(); + + flags = p.fetchFlags(getSequenceNumber()); + // make sure flags is always set, even if server is broken + if (flags == null) + flags = new Flags(); + } catch (ConnectionException cex) { + throw new FolderClosedException(folder, cex.getMessage()); + } catch (ProtocolException pex) { + forceCheckExpunged(); + throw new MessagingException(pex.getMessage(), pex); + } + } // Release MessageCacheLock + } + + /* + * Are all headers loaded? + */ + private boolean areHeadersLoaded() { + return headersLoaded; + } + + /* + * Set whether all headers are loaded. + */ + private void setHeadersLoaded(boolean loaded) { + headersLoaded = loaded; + } + + /* + * Check if the given header was ever loaded from the server + */ + private boolean isHeaderLoaded(String name) { + if (headersLoaded) // All headers for this message have been loaded + return true; + + return loadedHeaders.containsKey(name.toUpperCase(Locale.ENGLISH)); + } + + /* + * Mark that the given headers have been loaded from the server. + */ + private void setHeaderLoaded(String name) { + loadedHeaders.put(name.toUpperCase(Locale.ENGLISH), name); + } + + /* + * Convert the given FETCH item identifier to the approriate + * section-string for this message. + */ + private String toSection(String what) { + if (sectionId == null) + return what; + else + return sectionId + "." + what; + } + + /* + * Clone an array of InternetAddresses. + */ + private InternetAddress[] aaclone(InternetAddress[] aa) { + if (aa == null) + return null; + else + return aa.clone(); + } + + private Flags _getFlags() { + return flags; + } + + private ENVELOPE _getEnvelope() { + return envelope; + } + + private BODYSTRUCTURE _getBodyStructure() { + return bs; + } + + /*********************************************************** + * accessor routines to make available certain private/protected + * fields to other classes in this package. + ***********************************************************/ + + /* + * Called by IMAPFolder. + * Must not be synchronized. + */ + void _setFlags(Flags flags) { + this.flags = flags; + } + + /* + * Called by IMAPNestedMessage. + */ + Session _getSession() { + return session; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPMultipartDataSource.java b/app/src/main/java/com/sun/mail/imap/IMAPMultipartDataSource.java new file mode 100644 index 0000000000..c55ef022c2 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPMultipartDataSource.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + + +import javax.mail.*; +import javax.mail.internet.*; + +import com.sun.mail.imap.protocol.*; +import java.util.ArrayList; +import java.util.List; + +/** + * This class + * + * @author John Mani + */ + +public class IMAPMultipartDataSource extends MimePartDataSource + implements MultipartDataSource { + private List parts; + + protected IMAPMultipartDataSource(MimePart part, BODYSTRUCTURE[] bs, + String sectionId, IMAPMessage msg) { + super(part); + + parts = new ArrayList<>(bs.length); + for (int i = 0; i < bs.length; i++) + parts.add( + new IMAPBodyPart(bs[i], + sectionId == null ? + Integer.toString(i+1) : + sectionId + "." + Integer.toString(i+1), + msg) + ); + } + + @Override + public int getCount() { + return parts.size(); + } + + @Override + public BodyPart getBodyPart(int index) throws MessagingException { + return parts.get(index); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPNestedMessage.java b/app/src/main/java/com/sun/mail/imap/IMAPNestedMessage.java new file mode 100644 index 0000000000..12cd4e453a --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPNestedMessage.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.*; +import javax.mail.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.iap.ProtocolException; + +/** + * This class implements a nested IMAP message + * + * @author John Mani + */ + +public class IMAPNestedMessage extends IMAPMessage { + private IMAPMessage msg; // the enclosure of this nested message + + /** + * Package private constructor.

+ * + * Note that nested messages have no containing folder, nor + * a message number. + */ + IMAPNestedMessage(IMAPMessage m, BODYSTRUCTURE b, ENVELOPE e, String sid) { + super(m._getSession()); + msg = m; + bs = b; + envelope = e; + sectionId = sid; + setPeek(m.getPeek()); + } + + /* + * Get the enclosing message's Protocol object. Overrides + * IMAPMessage.getProtocol(). + */ + @Override + protected IMAPProtocol getProtocol() + throws ProtocolException, FolderClosedException { + return msg.getProtocol(); + } + + /* + * Is this an IMAP4 REV1 server? + */ + @Override + protected boolean isREV1() throws FolderClosedException { + return msg.isREV1(); + } + + /* + * Get the enclosing message's messageCacheLock. Overrides + * IMAPMessage.getMessageCacheLock(). + */ + @Override + protected Object getMessageCacheLock() { + return msg.getMessageCacheLock(); + } + + /* + * Get the enclosing message's sequence number. Overrides + * IMAPMessage.getSequenceNumber(). + */ + @Override + protected int getSequenceNumber() { + return msg.getSequenceNumber(); + } + + /* + * Check whether the enclosing message is expunged. Overrides + * IMAPMessage.checkExpunged(). + */ + @Override + protected void checkExpunged() throws MessageRemovedException { + msg.checkExpunged(); + } + + /* + * Check whether the enclosing message is expunged. Overrides + * Message.isExpunged(). + */ + @Override + public boolean isExpunged() { + return msg.isExpunged(); + } + + /* + * Get the enclosing message's fetchBlockSize. + */ + @Override + protected int getFetchBlockSize() { + return msg.getFetchBlockSize(); + } + + /* + * Get the enclosing message's ignoreBodyStructureSize. + */ + @Override + protected boolean ignoreBodyStructureSize() { + return msg.ignoreBodyStructureSize(); + } + + /* + * IMAPMessage uses RFC822.SIZE. We use the "size" field from + * our BODYSTRUCTURE. + */ + @Override + public int getSize() throws MessagingException { + return bs.size; + } + + /* + * Disallow setting flags on nested messages + */ + @Override + public synchronized void setFlags(Flags flag, boolean set) + throws MessagingException { + // Cannot set FLAGS on a nested IMAP message + throw new MethodNotSupportedException( + "Cannot set flags on this nested message"); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPProvider.java b/app/src/main/java/com/sun/mail/imap/IMAPProvider.java new file mode 100644 index 0000000000..0f35374380 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The IMAP protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class IMAPProvider extends Provider { + public IMAPProvider() { + super(Provider.Type.STORE, "imap", IMAPStore.class.getName(), + "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPSSLProvider.java b/app/src/main/java/com/sun/mail/imap/IMAPSSLProvider.java new file mode 100644 index 0000000000..354e63d490 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPSSLProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The IMAP SSL protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class IMAPSSLProvider extends Provider { + public IMAPSSLProvider() { + super(Provider.Type.STORE, "imaps", IMAPSSLStore.class.getName(), + "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPSSLStore.java b/app/src/main/java/com/sun/mail/imap/IMAPSSLStore.java new file mode 100644 index 0000000000..488b4ec13c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPSSLStore.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.*; + +/** + * This class provides access to an IMAP message store over SSL. + */ + +public class IMAPSSLStore extends IMAPStore { + + /** + * Constructor that takes a Session object and a URLName that + * represents a specific IMAP server. + * + * @param session the Session + * @param url the URLName of this store + */ + public IMAPSSLStore(Session session, URLName url) { + super(session, url, "imaps", true); // call super constructor + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IMAPStore.java b/app/src/main/java/com/sun/mail/imap/IMAPStore.java new file mode 100644 index 0000000000..866f177377 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IMAPStore.java @@ -0,0 +1,2211 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.lang.reflect.*; +import java.util.Vector; +import java.util.StringTokenizer; +import java.util.Locale; +import java.util.Properties; +import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.HashMap; +import java.util.logging.Level; + +import javax.mail.*; +import javax.mail.event.*; + +import com.sun.mail.iap.*; +import com.sun.mail.imap.protocol.*; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketConnectException; +import com.sun.mail.util.MailConnectException; +import java.util.ArrayList; +import java.util.List; + +/** + * This class provides access to an IMAP message store.

+ * + * Applications that need to make use of IMAP-specific features may cast + * a Store object to an IMAPStore object and + * use the methods on this class. The {@link #getQuota getQuota} and + * {@link #setQuota setQuota} methods support the IMAP QUOTA extension. + * Refer to RFC 2087 + * for more information.

+ * + * The {@link #id id} method supports the IMAP ID extension; + * see RFC 2971. + * The fields ID_NAME, ID_VERSION, etc. represent the suggested field names + * in RFC 2971 section 3.3 and may be used as keys in the Map containing + * client values or server values.

+ * + * See the com.sun.mail.imap package + * documentation for further information on the IMAP protocol provider.

+ * + * WARNING: The APIs unique to this class should be + * considered EXPERIMENTAL. They may be changed in the + * future in ways that are incompatible with applications using the + * current APIs. + * + * @author John Mani + * @author Bill Shannon + * @author Jim Glennon + */ +/* + * This package is implemented over the "imap.protocol" package, which + * implements the protocol-level commands.

+ * + * A connected IMAPStore maintains a pool of IMAP protocol objects for + * use in communicating with the IMAP server. The IMAPStore will create + * the initial AUTHENTICATED connection and seed the pool with this + * connection. As folders are opened and new IMAP protocol objects are + * needed, the IMAPStore will provide them from the connection pool, + * or create them if none are available. When a folder is closed, + * its IMAP protocol object is returned to the connection pool if the + * pool is not over capacity. The pool size can be configured by setting + * the mail.imap.connectionpoolsize property.

+ * + * Note that all connections in the connection pool have their response + * handler set to be the Store. When the connection is removed from the + * pool for use by a folder, the response handler is removed and then set + * to either the Folder or to the special nonStoreResponseHandler, depending + * on how the connection is being used. This is probably excessive. + * Better would be for the Protocol object to support only a single + * response handler, which would be set before the connection is used + * and cleared when the connection is in the pool and can't be used.

+ * + * A mechanism is provided for timing out idle connection pool IMAP + * protocol objects. Timed out connections are closed and removed (pruned) + * from the connection pool. The time out interval can be configured via + * the mail.imap.connectionpooltimeout property.

+ * + * The connected IMAPStore object may or may not maintain a separate IMAP + * protocol object that provides the store a dedicated connection to the + * IMAP server. This is provided mainly for compatibility with previous + * implementations of Jakarta Mail and is determined by the value of the + * mail.imap.separatestoreconnection property.

+ * + * An IMAPStore object provides closed IMAPFolder objects thru its list() + * and listSubscribed() methods. A closed IMAPFolder object acquires an + * IMAP protocol object from the store to communicate with the server. When + * the folder is opened, it gets its own protocol object and thus its own, + * separate connection to the server. The store maintains references to + * all 'open' folders. When a folder is/gets closed, the store removes + * it from its list. When the store is/gets closed, it closes all open + * folders in its list, thus cleaning up all open connections to the + * server.

+ * + * A mutex is used to control access to the connection pool resources. + * Any time any of these resources need to be accessed, the following + * convention should be followed: + * + * synchronized (pool) { // ACQUIRE LOCK + * // access connection pool resources + * } // RELEASE LOCK

+ * + * The locking relationship between the store and folders is that the + * store lock must be acquired before a folder lock. This is currently only + * applicable in the store's cleanup method. It's important that the + * connection pool lock is not held when calling into folder objects. + * The locking hierarchy is that a folder lock must be acquired before + * any connection pool operations are performed. You never need to hold + * all three locks, but if you hold more than one this is the order you + * have to acquire them in.

+ * + * That is: Store > Folder, Folder > pool, Store > pool

+ * + * The IMAPStore implements the ResponseHandler interface and listens to + * BYE or untagged OK-notification events from the server as a result of + * Store operations. IMAPFolder forwards notifications that result from + * Folder operations using the store connection; the IMAPStore ResponseHandler + * is not used directly in this case.

+ */ + +public class IMAPStore extends Store + implements QuotaAwareStore, ResponseHandler { + + /** + * A special event type for a StoreEvent to indicate an IMAP + * response, if the mail.imap.enableimapevents property is set. + */ + public static final int RESPONSE = 1000; + + public static final String ID_NAME = "name"; + public static final String ID_VERSION = "version"; + public static final String ID_OS = "os"; + public static final String ID_OS_VERSION = "os-version"; + public static final String ID_VENDOR = "vendor"; + public static final String ID_SUPPORT_URL = "support-url"; + public static final String ID_ADDRESS = "address"; + public static final String ID_DATE = "date"; + public static final String ID_COMMAND = "command"; + public static final String ID_ARGUMENTS = "arguments"; + public static final String ID_ENVIRONMENT = "environment"; + + protected final String name; // name of this protocol + protected final int defaultPort; // default IMAP port + protected final boolean isSSL; // use SSL? + + private final int blksize; // Block size for data requested + // in FETCH requests. Defaults to + // 16K + + private boolean ignoreSize; // ignore the size in BODYSTRUCTURE? + + private final int statusCacheTimeout; // cache Status for 1 second + + private final int appendBufferSize; // max size of msg buffered for append + + private final int minIdleTime; // minimum idle time + + private volatile int port = -1; // port to use + + // Auth info + protected String host; + protected String user; + protected String password; + protected String proxyAuthUser; + protected String authorizationID; + protected String saslRealm; + + private Namespaces namespaces; + + private boolean enableStartTLS = false; // enable STARTTLS + private boolean requireStartTLS = false; // require STARTTLS + private boolean usingSSL = false; // using SSL? + private boolean enableSASL = false; // enable SASL authentication + private String[] saslMechanisms; + private boolean forcePasswordRefresh = false; + // enable notification of IMAP responses + private boolean enableResponseEvents = false; + // enable notification of IMAP responses during IDLE + private boolean enableImapEvents = false; + private String guid; // for Yahoo! Mail IMAP + private boolean throwSearchException = false; + private boolean peek = false; + private boolean closeFoldersOnStoreFailure = true; + private boolean enableCompress = false; // enable COMPRESS=DEFLATE + private boolean finalizeCleanClose = false; + + /* + * This field is set in the Store's response handler if we see + * a BYE response. The releaseStore method checks this field + * and if set it cleans up the Store. Field is volatile because + * there's no lock we consistently hold while manipulating it. + * + * Because volatile doesn't really work before JDK 1.5, + * use a lock to protect these two fields. + */ + private volatile boolean connectionFailed = false; + private volatile boolean forceClose = false; + private final Object connectionFailedLock = new Object(); + + private boolean debugusername; // include username in debug output? + private boolean debugpassword; // include password in debug output? + protected MailLogger logger; // for debug output + + private boolean messageCacheDebug; + + // constructors for IMAPFolder class provided by user + private volatile Constructor folderConstructor = null; + private volatile Constructor folderConstructorLI = null; + + // Connection pool info + + static class ConnectionPool { + + // container for the pool's IMAP protocol objects + private Vector authenticatedConnections + = new Vector<>(); + + // vectore of open folders + private Vector folders; + + // is the store connection being used? + private boolean storeConnectionInUse = false; + + // the last time (in millis) the pool was checked for timed out + // connections + private long lastTimePruned; + + // flag to indicate whether there is a dedicated connection for + // store commands + private final boolean separateStoreConnection; + + // client timeout interval + private final long clientTimeoutInterval; + + // server timeout interval + private final long serverTimeoutInterval; + + // size of the connection pool + private final int poolSize; + + // interval for checking for timed out connections + private final long pruningInterval; + + // connection pool logger + private final MailLogger logger; + + /* + * The idleState field supports the IDLE command. + * Normally when executing an IMAP command we hold the + * store's lock. + * While executing the IDLE command we can't hold the + * lock or it would prevent other threads from + * entering Store methods even far enough to check whether + * an IDLE command is in progress. We need to check before + * issuing another command so that we can abort the IDLE + * command. + * + * The idleState field is protected by the store's lock. + * The RUNNING state is the normal state and means no IDLE + * command is in progress. The IDLE state means we've issued + * an IDLE command and are reading responses. The ABORTING + * state means we've sent the DONE continuation command and + * are waiting for the thread running the IDLE command to + * break out of its read loop. + * + * When an IDLE command is in progress, the thread calling + * the idle method will be reading from the IMAP connection + * while not holding the store's lock. + * It's obviously critical that no other thread try to send a + * command or read from the connection while in this state. + * However, other threads can send the DONE continuation + * command that will cause the server to break out of the IDLE + * loop and send the ending tag response to the IDLE command. + * The thread in the idle method that's reading the responses + * from the IDLE command will see this ending response and + * complete the idle method, setting the idleState field back + * to RUNNING, and notifying any threads waiting to use the + * connection. + * + * All uses of the IMAP connection (IMAPProtocol object) must + * be preceeded by a check to make sure an IDLE command is not + * running, and abort the IDLE command if necessary. This check + * is made while holding the connection pool lock. While + * waiting for the IDLE command to complete, these other threads + * will give up the connection pool lock. This check is done by + * the getStoreProtocol() method. + */ + private static final int RUNNING = 0; // not doing IDLE command + private static final int IDLE = 1; // IDLE command in effect + private static final int ABORTING = 2; // IDLE command aborting + private int idleState = RUNNING; + private IMAPProtocol idleProtocol; // protocol object when IDLE + + ConnectionPool(String name, MailLogger plogger, Session session) { + lastTimePruned = System.currentTimeMillis(); + Properties props = session.getProperties(); + + boolean debug = PropUtil.getBooleanProperty(props, + "mail." + name + ".connectionpool.debug", false); + logger = plogger.getSubLogger("connectionpool", + "DEBUG IMAP CP", debug); + + // check if the default connection pool size is overridden + int size = PropUtil.getIntProperty(props, + "mail." + name + ".connectionpoolsize", -1); + if (size > 0) { + poolSize = size; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.connectionpoolsize: " + poolSize); + } else + poolSize = 1; + + // check if the default client-side timeout value is overridden + int connectionPoolTimeout = PropUtil.getIntProperty(props, + "mail." + name + ".connectionpooltimeout", -1); + if (connectionPoolTimeout > 0) { + clientTimeoutInterval = connectionPoolTimeout; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.connectionpooltimeout: " + + clientTimeoutInterval); + } else + clientTimeoutInterval = 45 * 1000; // 45 seconds + + // check if the default server-side timeout value is overridden + int serverTimeout = PropUtil.getIntProperty(props, + "mail." + name + ".servertimeout", -1); + if (serverTimeout > 0) { + serverTimeoutInterval = serverTimeout; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.servertimeout: " + + serverTimeoutInterval); + } else + serverTimeoutInterval = 30 * 60 * 1000; // 30 minutes + + // check if the default server-side timeout value is overridden + int pruning = PropUtil.getIntProperty(props, + "mail." + name + ".pruninginterval", -1); + if (pruning > 0) { + pruningInterval = pruning; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.pruninginterval: " + + pruningInterval); + } else + pruningInterval = 60 * 1000; // 1 minute + + // check to see if we should use a separate (i.e. dedicated) + // store connection + separateStoreConnection = + PropUtil.getBooleanProperty(props, + "mail." + name + ".separatestoreconnection", false); + if (separateStoreConnection) + logger.config("dedicate a store connection"); + + } + } + + private final ConnectionPool pool; + + /** + * A special response handler for connections that are being used + * to perform operations on behalf of an object other than the Store. + * It DOESN'T cause the Store to be cleaned up if a BYE is seen. + * The BYE may be real or synthetic and in either case just indicates + * that the connection is dead. + */ + private ResponseHandler nonStoreResponseHandler = new ResponseHandler() { + @Override + public void handleResponse(Response r) { + // Any of these responses may have a response code. + if (r.isOK() || r.isNO() || r.isBAD() || r.isBYE()) + handleResponseCode(r); + if (r.isBYE()) + logger.fine("IMAPStore non-store connection dead"); + } + }; + + /** + * Constructor that takes a Session object and a URLName that + * represents a specific IMAP server. + * + * @param session the Session + * @param url the URLName of this store + */ + public IMAPStore(Session session, URLName url) { + this(session, url, "imap", false); + } + + /** + * Constructor used by this class and by IMAPSSLStore subclass. + * + * @param session the Session + * @param url the URLName of this store + * @param name the protocol name for this store + * @param isSSL use SSL? + */ + protected IMAPStore(Session session, URLName url, + String name, boolean isSSL) { + super(session, url); // call super constructor + Properties props = session.getProperties(); + + if (url != null) + name = url.getProtocol(); + this.name = name; + if (!isSSL) + isSSL = PropUtil.getBooleanProperty(props, + "mail." + name + ".ssl.enable", false); + if (isSSL) + this.defaultPort = 993; + else + this.defaultPort = 143; + this.isSSL = isSSL; + + debug = session.getDebug(); + debugusername = PropUtil.getBooleanProperty(props, + "mail.debug.auth.username", true); + debugpassword = PropUtil.getBooleanProperty(props, + "mail.debug.auth.password", false); + logger = new MailLogger(this.getClass(), + "DEBUG " + name.toUpperCase(Locale.ENGLISH), + session.getDebug(), session.getDebugOut()); + + boolean partialFetch = PropUtil.getBooleanProperty(props, + "mail." + name + ".partialfetch", true); + if (!partialFetch) { + blksize = -1; + logger.config("mail.imap.partialfetch: false"); + } else { + blksize = PropUtil.getIntProperty(props, + "mail." + name +".fetchsize", 1024 * 16); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.fetchsize: " + blksize); + } + + ignoreSize = PropUtil.getBooleanProperty(props, + "mail." + name +".ignorebodystructuresize", false); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.ignorebodystructuresize: " + ignoreSize); + + statusCacheTimeout = PropUtil.getIntProperty(props, + "mail." + name + ".statuscachetimeout", 1000); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.statuscachetimeout: " + + statusCacheTimeout); + + appendBufferSize = PropUtil.getIntProperty(props, + "mail." + name + ".appendbuffersize", -1); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.appendbuffersize: " + appendBufferSize); + + minIdleTime = PropUtil.getIntProperty(props, + "mail." + name + ".minidletime", 10); + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.minidletime: " + minIdleTime); + + // check if we should do a PROXYAUTH login + String s = session.getProperty("mail." + name + ".proxyauth.user"); + if (s != null) { + proxyAuthUser = s; + if (logger.isLoggable(Level.CONFIG)) + logger.config("mail.imap.proxyauth.user: " + proxyAuthUser); + } + + // check if STARTTLS is enabled + enableStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.enable", false); + if (enableStartTLS) + logger.config("enable STARTTLS"); + + // check if STARTTLS is required + requireStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.required", false); + if (requireStartTLS) + logger.config("require STARTTLS"); + + // check if SASL is enabled + enableSASL = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.enable", false); + if (enableSASL) + logger.config("enable SASL"); + + // check if SASL mechanisms are specified + if (enableSASL) { + s = session.getProperty("mail." + name + ".sasl.mechanisms"); + if (s != null && s.length() > 0) { + if (logger.isLoggable(Level.CONFIG)) + logger.config("SASL mechanisms allowed: " + s); + List v = new ArrayList<>(5); + StringTokenizer st = new StringTokenizer(s, " ,"); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + if (m.length() > 0) + v.add(m); + } + saslMechanisms = new String[v.size()]; + v.toArray(saslMechanisms); + } + } + + // check if an authorization ID has been specified + s = session.getProperty("mail." + name + ".sasl.authorizationid"); + if (s != null) { + authorizationID = s; + logger.log(Level.CONFIG, "mail.imap.sasl.authorizationid: {0}", + authorizationID); + } + + // check if a SASL realm has been specified + s = session.getProperty("mail." + name + ".sasl.realm"); + if (s != null) { + saslRealm = s; + logger.log(Level.CONFIG, "mail.imap.sasl.realm: {0}", saslRealm); + } + + // check if forcePasswordRefresh is enabled + forcePasswordRefresh = PropUtil.getBooleanProperty(props, + "mail." + name + ".forcepasswordrefresh", false); + if (forcePasswordRefresh) + logger.config("enable forcePasswordRefresh"); + + // check if enableimapevents is enabled + enableResponseEvents = PropUtil.getBooleanProperty(props, + "mail." + name + ".enableresponseevents", false); + if (enableResponseEvents) + logger.config("enable IMAP response events"); + + // check if enableresponseevents is enabled + enableImapEvents = PropUtil.getBooleanProperty(props, + "mail." + name + ".enableimapevents", false); + if (enableImapEvents) + logger.config("enable IMAP IDLE events"); + + // check if message cache debugging set + messageCacheDebug = PropUtil.getBooleanProperty(props, + "mail." + name + ".messagecache.debug", false); + + guid = session.getProperty("mail." + name + ".yahoo.guid"); + if (guid != null) + logger.log(Level.CONFIG, "mail.imap.yahoo.guid: {0}", guid); + + // check if throwsearchexception is enabled + throwSearchException = PropUtil.getBooleanProperty(props, + "mail." + name + ".throwsearchexception", false); + if (throwSearchException) + logger.config("throw SearchException"); + + // check if peek is set + peek = PropUtil.getBooleanProperty(props, + "mail." + name + ".peek", false); + if (peek) + logger.config("peek"); + + // check if closeFoldersOnStoreFailure is set + closeFoldersOnStoreFailure = PropUtil.getBooleanProperty(props, + "mail." + name + ".closefoldersonstorefailure", true); + if (closeFoldersOnStoreFailure) + logger.config("closeFoldersOnStoreFailure"); + + // check if COMPRESS is enabled + enableCompress = PropUtil.getBooleanProperty(props, + "mail." + name + ".compress.enable", false); + if (enableCompress) + logger.config("enable COMPRESS"); + + // check if finalizeCleanClose is enabled + finalizeCleanClose = PropUtil.getBooleanProperty(props, + "mail." + name + ".finalizecleanclose", false); + if (finalizeCleanClose) + logger.config("close connection cleanly in finalize"); + + s = session.getProperty("mail." + name + ".folder.class"); + if (s != null) { + logger.log(Level.CONFIG, "IMAP: folder class: {0}", s); + try { + ClassLoader cl = this.getClass().getClassLoader(); + + // now load the class + Class folderClass = null; + try { + // First try the "application's" class loader. + // This should eventually be replaced by + // Thread.currentThread().getContextClassLoader(). + folderClass = Class.forName(s, false, cl); + } catch (ClassNotFoundException ex1) { + // That didn't work, now try the "system" class loader. + // (Need both of these because JDK 1.1 class loaders + // may not delegate to their parent class loader.) + folderClass = Class.forName(s); + } + + Class[] c = { String.class, char.class, IMAPStore.class, + Boolean.class }; + folderConstructor = folderClass.getConstructor(c); + Class[] c2 = { ListInfo.class, IMAPStore.class }; + folderConstructorLI = folderClass.getConstructor(c2); + } catch (Exception ex) { + logger.log(Level.CONFIG, + "IMAP: failed to load folder class", ex); + } + } + + pool = new ConnectionPool(name, logger, session); + } + + /** + * Implementation of protocolConnect(). Will create a connection + * to the server and authenticate the user using the mechanisms + * specified by various properties.

+ * + * The host, user, and password + * parameters must all be non-null. If the authentication mechanism + * being used does not require a password, an empty string or other + * suitable dummy password should be used. + */ + @Override + protected synchronized boolean + protocolConnect(String host, int pport, String user, String password) + throws MessagingException { + + IMAPProtocol protocol = null; + + // check for non-null values of host, password, user + if (host == null || password == null || user == null) { + if (logger.isLoggable(Level.FINE)) + logger.fine("protocolConnect returning false" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + return false; + } + + // set the port correctly + if (pport != -1) { + port = pport; + } else { + port = PropUtil.getIntProperty(session.getProperties(), + "mail." + name + ".port", port); + } + + // use the default if needed + if (port == -1) { + port = defaultPort; + } + + try { + boolean poolEmpty; + synchronized (pool) { + poolEmpty = pool.authenticatedConnections.isEmpty(); + } + + if (poolEmpty) { + if (logger.isLoggable(Level.FINE)) + logger.fine("trying to connect to host \"" + host + + "\", port " + port + ", isSSL " + isSSL); + protocol = newIMAPProtocol(host, port); + if (logger.isLoggable(Level.FINE)) + logger.fine("protocolConnect login" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + protocol.addResponseHandler(nonStoreResponseHandler); + login(protocol, user, password); + protocol.removeResponseHandler(nonStoreResponseHandler); + protocol.addResponseHandler(this); + + usingSSL = protocol.isSSL(); // in case anyone asks + + this.host = host; + this.user = user; + this.password = password; + + synchronized (pool) { + pool.authenticatedConnections.addElement(protocol); + } + } + } catch (IMAPReferralException ex) { + // login failure due to IMAP REFERRAL, close connection to server + if (protocol != null) + protocol.disconnect(); + protocol = null; + throw new ReferralException(ex.getUrl(), ex.getMessage()); + } catch (CommandFailedException cex) { + // login failure, close connection to server + if (protocol != null) + protocol.disconnect(); + protocol = null; + Response r = cex.getResponse(); + throw new AuthenticationFailedException( + r != null ? r.getRest() : cex.getMessage()); + } catch (ProtocolException pex) { // any other exception + // failure in login command, close connection to server + if (protocol != null) + protocol.disconnect(); + protocol = null; + throw new MessagingException(pex.getMessage(), pex); + } catch (SocketConnectException scex) { + throw new MailConnectException(scex); + } catch (IOException ioex) { + throw new MessagingException(ioex.getMessage(), ioex); + } + + return true; + } + + /** + * Create an IMAPProtocol object connected to the host and port. + * Subclasses of IMAPStore may override this method to return a + * subclass of IMAPProtocol that supports product-specific extensions. + * + * @param host the host name + * @param port the port number + * @return the new IMAPProtocol object + * @exception IOException for I/O errors + * @exception ProtocolException for protocol errors + * @since JavaMail 1.4.6 + */ + protected IMAPProtocol newIMAPProtocol(String host, int port) + throws IOException, ProtocolException { + return new IMAPProtocol(name, host, port, + session.getProperties(), + isSSL, + logger + ); + } + + private void login(IMAPProtocol p, String u, String pw) + throws ProtocolException { + // turn on TLS if it's been enabled or required and is supported + // and we're not already using SSL + if ((enableStartTLS || requireStartTLS) && !p.isSSL()) { + if (p.hasCapability("STARTTLS")) { + p.startTLS(); + // if startTLS succeeds, refresh capabilities + p.capability(); + } else if (requireStartTLS) { + logger.fine("STARTTLS required but not supported by server"); + throw new ProtocolException( + "STARTTLS required but not supported by server"); + } + } + if (p.isAuthenticated()) + return; // no need to login + + // allow subclasses to issue commands before login + preLogin(p); + + // issue special ID command to Yahoo! Mail IMAP server + // http://en.wikipedia.org/wiki/Yahoo%21_Mail#Free_IMAP_and_SMTPs_access + if (guid != null) { + Map gmap = new HashMap<>(); + gmap.put("GUID", guid); + p.id(gmap); + } + + /* + * Put a special "marker" in the capabilities list so we can + * detect if the server refreshed the capabilities in the OK + * response. + */ + p.getCapabilities().put("__PRELOGIN__", ""); + String authzid; + if (authorizationID != null) + authzid = authorizationID; + else if (proxyAuthUser != null) + authzid = proxyAuthUser; + else + authzid = null; + + if (enableSASL) { + try { + p.sasllogin(saslMechanisms, saslRealm, authzid, u, pw); + if (!p.isAuthenticated()) + throw new CommandFailedException( + "SASL authentication failed"); + } catch (UnsupportedOperationException ex) { + // continue to try other authentication methods below + } + } + + if (!p.isAuthenticated()) + authenticate(p, authzid, u, pw); + + if (proxyAuthUser != null) + p.proxyauth(proxyAuthUser); + + /* + * If marker is still there, capabilities haven't been refreshed, + * refresh them now. + */ + if (p.hasCapability("__PRELOGIN__")) { + try { + p.capability(); + } catch (ConnectionException cex) { + throw cex; // rethrow connection failures + // XXX - assume connection has been closed + } catch (ProtocolException pex) { + // ignore other exceptions that "should never happen" + } + } + + if (enableCompress) { + if (p.hasCapability("COMPRESS=DEFLATE")) { + p.compress(); + } + } + + // if server supports UTF-8, enable it for client use + // note that this is safe to enable even if mail.mime.allowutf8=false + if (p.hasCapability("UTF8=ACCEPT") || p.hasCapability("UTF8=ONLY")) + p.enable("UTF8=ACCEPT"); + } + + /** + * Authenticate using one of the non-SASL mechanisms. + * + * @param p the IMAPProtocol object + * @param authzid the authorization ID + * @param user the user name + * @param password the password + * @exception ProtocolException on failures + */ + private void authenticate(IMAPProtocol p, String authzid, + String user, String password) + throws ProtocolException { + // this list must match the "if" statements below + String defaultAuthenticationMechanisms = "PLAIN LOGIN NTLM XOAUTH2"; + + // setting mail.imap.auth.mechanisms controls which mechanisms will + // be used, and in what order they'll be considered. only the first + // match is used. + String mechs = session.getProperty("mail." + name + ".auth.mechanisms"); + + if (mechs == null) + mechs = defaultAuthenticationMechanisms; + + /* + * Loop through the list of mechanisms supplied by the user + * (or defaulted) and try each in turn. If the server supports + * the mechanism and we have an authenticator for the mechanism, + * and it hasn't been disabled, use it. + */ + StringTokenizer st = new StringTokenizer(mechs); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + m = m.toUpperCase(Locale.ENGLISH); + + /* + * If using the default mechanisms, check if this one is disabled. + */ + if (mechs == defaultAuthenticationMechanisms) { + String dprop = "mail." + name + ".auth." + + m.toLowerCase(Locale.ENGLISH) + ".disable"; + boolean disabled = PropUtil.getBooleanProperty( + session.getProperties(), + dprop, m.equals("XOAUTH2")); + if (disabled) { + if (logger.isLoggable(Level.FINE)) + logger.fine("mechanism " + m + + " disabled by property: " + dprop); + continue; + } + } + + if (!(p.hasCapability("AUTH=" + m) || + (m.equals("LOGIN") && p.hasCapability("AUTH-LOGIN")))) { + logger.log(Level.FINE, "mechanism {0} not supported by server", + m); + continue; + } + + if (m.equals("PLAIN")) + p.authplain(authzid, user, password); + else if (m.equals("LOGIN")) + p.authlogin(user, password); + else if (m.equals("NTLM")) + p.authntlm(authzid, user, password); + else if (m.equals("XOAUTH2")) + p.authoauth2(user, password); + else { + logger.log(Level.FINE, "no authenticator for mechanism {0}", m); + continue; + } + return; + } + + if (!p.hasCapability("LOGINDISABLED")) { + p.login(user, password); + return; + } + + throw new ProtocolException("No login methods supported!"); + } + + /** + * This method is called after the connection is made and + * TLS is started (if needed), but before any authentication + * is attempted. Subclasses can override this method to + * issue commands that are needed in the "not authenticated" + * state. Note that if the connection is pre-authenticated, + * this method won't be called.

+ * + * The implementation of this method in this class does nothing. + * + * @param p the IMAPProtocol connection + * @exception ProtocolException for protocol errors + * @since JavaMail 1.4.4 + */ + protected void preLogin(IMAPProtocol p) throws ProtocolException { + } + + /** + * Does this IMAPStore use SSL when connecting to the server? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public synchronized boolean isSSL() { + return usingSSL; + } + + /** + * Set the user name that will be used for subsequent connections + * after this Store is first connected (for example, when creating + * a connection to open a Folder). This value is overridden + * by any call to the Store's connect method.

+ * + * Some IMAP servers may provide an authentication ID that can + * be used for more efficient authentication for future connections. + * This authentication ID is provided in a server-specific manner + * not described here.

+ * + * Most applications will never need to use this method. + * + * @param user the user name for the store + * @since JavaMail 1.3.3 + */ + public synchronized void setUsername(String user) { + this.user = user; + } + + /** + * Set the password that will be used for subsequent connections + * after this Store is first connected (for example, when creating + * a connection to open a Folder). This value is overridden + * by any call to the Store's connect method.

+ * + * Most applications will never need to use this method. + * + * @param password the password for the store + * @since JavaMail 1.3.3 + */ + public synchronized void setPassword(String password) { + this.password = password; + } + + /* + * Get a new authenticated protocol object for this Folder. + * Also store a reference to this folder in our list of + * open folders. + */ + IMAPProtocol getProtocol(IMAPFolder folder) + throws MessagingException { + IMAPProtocol p = null; + + // keep looking for a connection until we get a good one + while (p == null) { + + // New authenticated protocol objects are either acquired + // from the connection pool, or created when the pool is + // empty or no connections are available. None are available + // if the current pool size is one and the separate store + // property is set or the connection is in use. + + synchronized (pool) { + + // If there's none available in the pool, + // create a new one. + if (pool.authenticatedConnections.isEmpty() || + (pool.authenticatedConnections.size() == 1 && + (pool.separateStoreConnection || pool.storeConnectionInUse))) { + + logger.fine("no connections in the pool, creating a new one"); + try { + if (forcePasswordRefresh) + refreshPassword(); + // Use cached host, port and timeout values. + p = newIMAPProtocol(host, port); + p.addResponseHandler(nonStoreResponseHandler); + // Use cached auth info + login(p, user, password); + p.removeResponseHandler(nonStoreResponseHandler); + } catch(Exception ex1) { + if (p != null) + try { + p.disconnect(); + } catch (Exception ex2) { } + p = null; + } + + if (p == null) + throw new MessagingException("connection failure"); + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("connection available -- size: " + + pool.authenticatedConnections.size()); + + // remove the available connection from the Authenticated queue + p = pool.authenticatedConnections.lastElement(); + pool.authenticatedConnections.removeElement(p); + + // check if the connection is still live + long lastUsed = System.currentTimeMillis() - p.getTimestamp(); + if (lastUsed > pool.serverTimeoutInterval) { + try { + /* + * Swap in a special response handler that will handle + * alerts, but won't cause the store to be closed and + * cleaned up if the connection is dead. + */ + p.removeResponseHandler(this); + p.addResponseHandler(nonStoreResponseHandler); + p.noop(); + p.removeResponseHandler(nonStoreResponseHandler); + p.addResponseHandler(this); + } catch (ProtocolException pex) { + try { + p.removeResponseHandler(nonStoreResponseHandler); + p.disconnect(); + } catch (RuntimeException ignored) { + // don't let any exception stop us + } + p = null; + continue; // try again, from the top + } + } + + // if proxyAuthUser has changed, switch to new user + if (proxyAuthUser != null && + !proxyAuthUser.equals(p.getProxyAuthUser()) && + p.hasCapability("X-UNAUTHENTICATE")) { + try { + /* + * Swap in a special response handler that will handle + * alerts, but won't cause the store to be closed and + * cleaned up if the connection is dead. + */ + p.removeResponseHandler(this); + p.addResponseHandler(nonStoreResponseHandler); + p.unauthenticate(); + login(p, user, password); + p.removeResponseHandler(nonStoreResponseHandler); + p.addResponseHandler(this); + } catch (ProtocolException pex) { + try { + p.removeResponseHandler(nonStoreResponseHandler); + p.disconnect(); + } catch (RuntimeException ignored) { + // don't let any exception stop us + } + p = null; + continue; // try again, from the top + } + } + + // remove the store as a response handler. + p.removeResponseHandler(this); + } + + // check if we need to look for client-side timeouts + timeoutConnections(); + + // Add folder to folder-list + if (folder != null) { + if (pool.folders == null) + pool.folders = new Vector<>(); + pool.folders.addElement(folder); + } + } + + } + + return p; + } + + /** + * Get this Store's protocol connection. + * + * When acquiring a store protocol object, it is important to + * use the following steps: + * + * IMAPProtocol p = null; + * try { + * p = getStoreProtocol(); + * // perform the command + * } catch (ConnectionException cex) { + * throw new StoreClosedException(this, cex.getMessage()); + * } catch (WhateverException ex) { + * // handle it + * } finally { + * releaseStoreProtocol(p); + * } + */ + private IMAPProtocol getStoreProtocol() throws ProtocolException { + IMAPProtocol p = null; + + while (p == null) { + synchronized (pool) { + waitIfIdle(); + + // If there's no authenticated connections available create a + // new one and place it in the authenticated queue. + if (pool.authenticatedConnections.isEmpty()) { + pool.logger.fine("getStoreProtocol() - no connections " + + "in the pool, creating a new one"); + try { + if (forcePasswordRefresh) + refreshPassword(); + // Use cached host, port and timeout values. + p = newIMAPProtocol(host, port); + // Use cached auth info + login(p, user, password); + } catch(Exception ex1) { + if (p != null) + try { + p.logout(); + } catch (Exception ex2) { } + p = null; + } + + if (p == null) + throw new ConnectionException( + "failed to create new store connection"); + + p.addResponseHandler(this); + pool.authenticatedConnections.addElement(p); + + } else { + // Always use the first element in the Authenticated queue. + if (pool.logger.isLoggable(Level.FINE)) + pool.logger.fine("getStoreProtocol() - " + + "connection available -- size: " + + pool.authenticatedConnections.size()); + p = pool.authenticatedConnections.firstElement(); + + // if proxyAuthUser has changed, switch to new user + if (proxyAuthUser != null && + !proxyAuthUser.equals(p.getProxyAuthUser()) && + p.hasCapability("X-UNAUTHENTICATE")) { + p.unauthenticate(); + login(p, user, password); + } + } + + if (pool.storeConnectionInUse) { + try { + // someone else is using the connection, give up + // and wait until they're done + p = null; + pool.wait(); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might + // depend on + Thread.currentThread().interrupt(); + // don't keep looking for a connection if we've been + // interrupted + throw new ProtocolException( + "Interrupted getStoreProtocol", ex); + } + } else { + pool.storeConnectionInUse = true; + + pool.logger.fine("getStoreProtocol() -- storeConnectionInUse"); + } + + timeoutConnections(); + } + } + return p; + } + + /** + * Get a store protocol object for use by a folder. + */ + IMAPProtocol getFolderStoreProtocol() throws ProtocolException { + IMAPProtocol p = getStoreProtocol(); + p.removeResponseHandler(this); + p.addResponseHandler(nonStoreResponseHandler); + return p; + } + + /* + * Some authentication systems use one time passwords + * or tokens, so each authentication request requires + * a new password. This "kludge" allows a callback + * to application code to get a new password. + * + * XXX - remove this when SASL support is added + */ + private void refreshPassword() { + if (logger.isLoggable(Level.FINE)) + logger.fine("refresh password, user: " + traceUser(user)); + InetAddress addr; + try { + addr = InetAddress.getByName(host); + } catch (UnknownHostException e) { + addr = null; + } + PasswordAuthentication pa = + session.requestPasswordAuthentication(addr, port, + name, null, user); + if (pa != null) { + user = pa.getUserName(); + password = pa.getPassword(); + } + } + + /** + * If a SELECT succeeds, but indicates that the folder is + * READ-ONLY, and the user asked to open the folder READ_WRITE, + * do we allow the open to succeed? + */ + boolean allowReadOnlySelect() { + return PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".allowreadonlyselect", false); + } + + /** + * Report whether the separateStoreConnection is set. + */ + boolean hasSeparateStoreConnection() { + return pool.separateStoreConnection; + } + + /** + * Return the connection pool logger. + */ + MailLogger getConnectionPoolLogger() { + return pool.logger; + } + + /** + * Report whether message cache debugging is enabled. + */ + boolean getMessageCacheDebug() { + return messageCacheDebug; + } + + /** + * Report whether the connection pool is full. + */ + boolean isConnectionPoolFull() { + + synchronized (pool) { + if (pool.logger.isLoggable(Level.FINE)) + pool.logger.fine("connection pool current size: " + + pool.authenticatedConnections.size() + + " pool size: " + pool.poolSize); + + return (pool.authenticatedConnections.size() >= pool.poolSize); + + } + } + + /** + * Release the protocol object back to the connection pool. + */ + void releaseProtocol(IMAPFolder folder, IMAPProtocol protocol) { + + synchronized (pool) { + if (protocol != null) { + // If the pool is not full, add the store as a response handler + // and return the protocol object to the connection pool. + if (!isConnectionPoolFull()) { + protocol.addResponseHandler(this); + pool.authenticatedConnections.addElement(protocol); + + if (logger.isLoggable(Level.FINE)) + logger.fine( + "added an Authenticated connection -- size: " + + pool.authenticatedConnections.size()); + } else { + logger.fine( + "pool is full, not adding an Authenticated connection"); + try { + protocol.logout(); + } catch (ProtocolException pex) {}; + } + } + + if (pool.folders != null) + pool.folders.removeElement(folder); + + timeoutConnections(); + } + } + + /** + * Release the store connection. + */ + private void releaseStoreProtocol(IMAPProtocol protocol) { + + // will be called from idle() without the Store lock held, + // but cleanup is synchronized and will acquire the Store lock + + if (protocol == null) { + cleanup(); // failed to ever get the connection + return; // nothing to release + } + + /* + * Read out the flag that says whether this connection failed + * before releasing the protocol object for others to use. + */ + boolean failed; + synchronized (connectionFailedLock) { + failed = connectionFailed; + connectionFailed = false; // reset for next use + } + + // now free the store connection + synchronized (pool) { + pool.storeConnectionInUse = false; + pool.notifyAll(); // in case anyone waiting + + pool.logger.fine("releaseStoreProtocol()"); + + timeoutConnections(); + } + + /* + * If the connection died while we were using it, clean up. + * It's critical that the store connection be freed and the + * connection pool not be locked while we do this. + */ + assert !Thread.holdsLock(pool); + if (failed) + cleanup(); + } + + /** + * Release a store protocol object that was being used by a folder. + */ + void releaseFolderStoreProtocol(IMAPProtocol protocol) { + if (protocol == null) + return; // should never happen + protocol.removeResponseHandler(nonStoreResponseHandler); + protocol.addResponseHandler(this); + synchronized (pool) { + pool.storeConnectionInUse = false; + pool.notifyAll(); // in case anyone waiting + + pool.logger.fine("releaseFolderStoreProtocol()"); + + timeoutConnections(); + } + } + + /** + * Empty the connection pool. + */ + private void emptyConnectionPool(boolean force) { + + synchronized (pool) { + for (int index = pool.authenticatedConnections.size() - 1; + index >= 0; --index) { + try { + IMAPProtocol p = + pool.authenticatedConnections.elementAt(index); + p.removeResponseHandler(this); + if (force) + p.disconnect(); + else + p.logout(); + } catch (ProtocolException pex) {}; + } + + pool.authenticatedConnections.removeAllElements(); + } + + pool.logger.fine("removed all authenticated connections from pool"); + } + + /** + * Check to see if it's time to shrink the connection pool. + */ + private void timeoutConnections() { + + synchronized (pool) { + + // If we've exceeded the pruning interval, look for stale + // connections to logout. + if (System.currentTimeMillis() - pool.lastTimePruned > + pool.pruningInterval && + pool.authenticatedConnections.size() > 1) { + + if (pool.logger.isLoggable(Level.FINE)) { + pool.logger.fine("checking for connections to prune: " + + (System.currentTimeMillis() - pool.lastTimePruned)); + pool.logger.fine("clientTimeoutInterval: " + + pool.clientTimeoutInterval); + } + + IMAPProtocol p; + + // Check the timestamp of the protocol objects in the pool and + // logout if the interval exceeds the client timeout value + // (leave the first connection). + for (int index = pool.authenticatedConnections.size() - 1; + index > 0; index--) { + p = pool.authenticatedConnections. + elementAt(index); + if (pool.logger.isLoggable(Level.FINE)) + pool.logger.fine("protocol last used: " + + (System.currentTimeMillis() - p.getTimestamp())); + if (System.currentTimeMillis() - p.getTimestamp() > + pool.clientTimeoutInterval) { + + pool.logger.fine( + "authenticated connection timed out, " + + "logging out the connection"); + + p.removeResponseHandler(this); + pool.authenticatedConnections.removeElementAt(index); + + try { + p.logout(); + } catch (ProtocolException pex) {} + } + } + pool.lastTimePruned = System.currentTimeMillis(); + } + } + } + + /** + * Get the block size to use for fetch requests on this Store. + */ + int getFetchBlockSize() { + return blksize; + } + + /** + * Ignore the size reported in the BODYSTRUCTURE when fetching data? + */ + boolean ignoreBodyStructureSize() { + return ignoreSize; + } + + /** + * Get a reference to the session. + */ + Session getSession() { + return session; + } + + /** + * Get the number of milliseconds to cache STATUS response. + */ + int getStatusCacheTimeout() { + return statusCacheTimeout; + } + + /** + * Get the maximum size of a message to buffer for append. + */ + int getAppendBufferSize() { + return appendBufferSize; + } + + /** + * Get the minimum amount of time to delay when returning from idle. + */ + int getMinIdleTime() { + return minIdleTime; + } + + /** + * Throw a SearchException if the search expression is too complex? + */ + boolean throwSearchException() { + return throwSearchException; + } + + /** + * Get the default "peek" value. + */ + boolean getPeek() { + return peek; + } + + /** + * Return true if the specified capability string is in the list + * of capabilities the server announced. + * + * @param capability the capability string + * @return true if the server supports this capability + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public synchronized boolean hasCapability(String capability) + throws MessagingException { + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + return p.hasCapability(capability); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + + /** + * Set the user name to be used with the PROXYAUTH command. + * The PROXYAUTH user name can also be set using the + * mail.imap.proxyauth.user property when this + * Store is created. + * + * @param user the user name to set + * @since JavaMail 1.5.1 + */ + public void setProxyAuthUser(String user) { + proxyAuthUser = user; + } + + /** + * Get the user name to be used with the PROXYAUTH command. + * + * @return the user name + * @since JavaMail 1.5.1 + */ + public String getProxyAuthUser() { + return proxyAuthUser; + } + + /** + * Check whether this store is connected. Override superclass + * method, to actually ping our server connection. + */ + @Override + public synchronized boolean isConnected() { + if (!super.isConnected()) { + // if we haven't been connected at all, don't bother with + // the NOOP. + return false; + } + + /* + * The below noop() request can: + * (1) succeed - in which case all is fine. + * + * (2) fail because the server returns NO or BAD, in which + * case we ignore it since we can't really do anything. + * (2) fail because a BYE response is obtained from the + * server + * (3) fail because the socket.write() to the server fails, + * in which case the iap.protocol() code converts the + * IOException into a BYE response. + * + * Thus, our BYE handler will take care of closing the Store + * in case our connection is really gone. + */ + + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + p.noop(); + } catch (ProtocolException pex) { + // will return false below + } finally { + releaseStoreProtocol(p); + } + + + return super.isConnected(); + } + + /** + * Close this Store. + */ + @Override + public synchronized void close() throws MessagingException { + cleanup(); + // do these again in case cleanup returned early + // because we were already closed due to a failure, + // in which case we force close everything + closeAllFolders(true); + emptyConnectionPool(true); + } + + @Override + protected void finalize() throws Throwable { + if (!finalizeCleanClose) { + // when finalizing, close connections abruptly + synchronized (connectionFailedLock) { + connectionFailed = true; + forceClose = true; + } + closeFoldersOnStoreFailure = true; // make sure folders get closed + } + try { + close(); + } finally { + super.finalize(); + } + } + + /** + * Cleanup before dying. + */ + private synchronized void cleanup() { + // if we're not connected, someone beat us to it + if (!super.isConnected()) { + logger.fine("IMAPStore cleanup, not connected"); + return; + } + + /* + * If forceClose is true, some thread ran into an error that suggests + * the server might be dead, so we force the folders to close + * abruptly without waiting for the server. Used when + * the store connection times out, for example. + */ + boolean force; + synchronized (connectionFailedLock) { + force = forceClose; + forceClose = false; + connectionFailed = false; + } + if (logger.isLoggable(Level.FINE)) + logger.fine("IMAPStore cleanup, force " + force); + + if (!force || closeFoldersOnStoreFailure) { + closeAllFolders(force); + } + + emptyConnectionPool(force); + + // to set the state and send the closed connection event + try { + super.close(); + } catch (MessagingException mex) { + // ignore it + } + logger.fine("IMAPStore cleanup done"); + } + + /** + * Close all open Folders. If force is true, close them forcibly. + */ + private void closeAllFolders(boolean force) { + List foldersCopy = null; + boolean done = true; + + // To avoid violating the locking hierarchy, there's no lock we + // can hold that prevents another thread from trying to open a + // folder at the same time we're trying to close all the folders. + // Thus, there's an inherent race condition here. We close all + // the folders we know about and then check whether any new folders + // have been opened in the mean time. We keep trying until we're + // successful in closing all the folders. + for (;;) { + // Make a copy of the folders list so we do not violate the + // folder-connection pool locking hierarchy. + synchronized (pool) { + if (pool.folders != null) { + done = false; + foldersCopy = pool.folders; + pool.folders = null; + } else { + done = true; + } + } + if (done) + break; + + // Close and remove any open folders under this Store. + for (int i = 0, fsize = foldersCopy.size(); i < fsize; i++) { + IMAPFolder f = foldersCopy.get(i); + + try { + if (force) { + logger.fine("force folder to close"); + // Don't want to wait for folder connection to timeout + // (if, for example, the server is down) so we close + // folders abruptly. + f.forceClose(); + } else { + logger.fine("close folder"); + f.close(false); + } + } catch (MessagingException mex) { + // Who cares ?! Ignore 'em. + } catch (IllegalStateException ex) { + // Ditto + } + } + + } + } + + /** + * Get the default folder, representing the root of this user's + * namespace. Returns a closed DefaultFolder object. + */ + @Override + public synchronized Folder getDefaultFolder() throws MessagingException { + checkConnected(); + return new DefaultFolder(this); + } + + /** + * Get named folder. Returns a new, closed IMAPFolder. + */ + @Override + public synchronized Folder getFolder(String name) + throws MessagingException { + checkConnected(); + return newIMAPFolder(name, IMAPFolder.UNKNOWN_SEPARATOR); + } + + /** + * Get named folder. Returns a new, closed IMAPFolder. + */ + @Override + public synchronized Folder getFolder(URLName url) + throws MessagingException { + checkConnected(); + return newIMAPFolder(url.getFile(), IMAPFolder.UNKNOWN_SEPARATOR); + } + + /** + * Create an IMAPFolder object. If user supplied their own class, + * use it. Otherwise, call the constructor. + * + * @param fullName the full name of the folder + * @param separator the separator character for the folder hierarchy + * @param isNamespace does this name represent a namespace? + * @return the new IMAPFolder object + */ + protected IMAPFolder newIMAPFolder(String fullName, char separator, + Boolean isNamespace) { + IMAPFolder f = null; + if (folderConstructor != null) { + try { + Object[] o = + { fullName, Character.valueOf(separator), this, isNamespace }; + f = (IMAPFolder)folderConstructor.newInstance(o); + } catch (Exception ex) { + logger.log(Level.FINE, + "exception creating IMAPFolder class", ex); + } + } + if (f == null) + f = new IMAPFolder(fullName, separator, this, isNamespace); + return f; + } + + /** + * Create an IMAPFolder object. Call the newIMAPFolder method + * above with a null isNamespace. + * + * @param fullName the full name of the folder + * @param separator the separator character for the folder hierarchy + * @return the new IMAPFolder object + */ + protected IMAPFolder newIMAPFolder(String fullName, char separator) { + return newIMAPFolder(fullName, separator, null); + } + + /** + * Create an IMAPFolder object. If user supplied their own class, + * use it. Otherwise, call the constructor. + * + * @param li the ListInfo for the folder + * @return the new IMAPFolder object + */ + protected IMAPFolder newIMAPFolder(ListInfo li) { + IMAPFolder f = null; + if (folderConstructorLI != null) { + try { + Object[] o = { li, this }; + f = (IMAPFolder)folderConstructorLI.newInstance(o); + } catch (Exception ex) { + logger.log(Level.FINE, + "exception creating IMAPFolder class LI", ex); + } + } + if (f == null) + f = new IMAPFolder(li, this); + return f; + } + + /** + * Using the IMAP NAMESPACE command (RFC 2342), return a set + * of folders representing the Personal namespaces. + */ + @Override + public Folder[] getPersonalNamespaces() throws MessagingException { + Namespaces ns = getNamespaces(); + if (ns == null || ns.personal == null) + return super.getPersonalNamespaces(); + return namespaceToFolders(ns.personal, null); + } + + /** + * Using the IMAP NAMESPACE command (RFC 2342), return a set + * of folders representing the User's namespaces. + */ + @Override + public Folder[] getUserNamespaces(String user) + throws MessagingException { + Namespaces ns = getNamespaces(); + if (ns == null || ns.otherUsers == null) + return super.getUserNamespaces(user); + return namespaceToFolders(ns.otherUsers, user); + } + + /** + * Using the IMAP NAMESPACE command (RFC 2342), return a set + * of folders representing the Shared namespaces. + */ + @Override + public Folder[] getSharedNamespaces() throws MessagingException { + Namespaces ns = getNamespaces(); + if (ns == null || ns.shared == null) + return super.getSharedNamespaces(); + return namespaceToFolders(ns.shared, null); + } + + private synchronized Namespaces getNamespaces() throws MessagingException { + checkConnected(); + + IMAPProtocol p = null; + + if (namespaces == null) { + try { + p = getStoreProtocol(); + namespaces = p.namespace(); + } catch (BadCommandException bex) { + // NAMESPACE not supported, ignore it + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + return namespaces; + } + + private Folder[] namespaceToFolders(Namespaces.Namespace[] ns, + String user) { + Folder[] fa = new Folder[ns.length]; + for (int i = 0; i < fa.length; i++) { + String name = ns[i].prefix; + if (user == null) { + // strip trailing delimiter + int len = name.length(); + if ( len > 0 && name.charAt(len - 1) == ns[i].delimiter) + name = name.substring(0, len - 1); + } else { + // add user + name += user; + } + fa[i] = newIMAPFolder(name, ns[i].delimiter, + Boolean.valueOf(user == null)); + } + return fa; + } + + /** + * Get the quotas for the named quota root. + * Quotas are controlled on the basis of a quota root, not + * (necessarily) a folder. The relationship between folders + * and quota roots depends on the IMAP server. Some servers + * might implement a single quota root for all folders owned by + * a user. Other servers might implement a separate quota root + * for each folder. A single folder can even have multiple + * quota roots, perhaps controlling quotas for different + * resources. + * + * @param root the name of the quota root + * @return array of Quota objects + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + @Override + public synchronized Quota[] getQuota(String root) + throws MessagingException { + checkConnected(); + Quota[] qa = null; + + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + qa = p.getQuotaRoot(root); + } catch (BadCommandException bex) { + throw new MessagingException("QUOTA not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + return qa; + } + + /** + * Set the quotas for the quota root specified in the quota argument. + * Typically this will be one of the quota roots obtained from the + * getQuota method, but it need not be. + * + * @param quota the quota to set + * @exception MessagingException if the server doesn't support the + * QUOTA extension + */ + @Override + public synchronized void setQuota(Quota quota) throws MessagingException { + checkConnected(); + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + p.setQuota(quota); + } catch (BadCommandException bex) { + throw new MessagingException("QUOTA not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + } + + private void checkConnected() { + assert Thread.holdsLock(this); + if (!super.isConnected()) + throw new IllegalStateException("Not connected"); + } + + /** + * Response handler method. + */ + @Override + public void handleResponse(Response r) { + // Any of these responses may have a response code. + if (r.isOK() || r.isNO() || r.isBAD() || r.isBYE()) + handleResponseCode(r); + if (r.isBYE()) { + logger.fine("IMAPStore connection dead"); + // Store's IMAP connection is dead, save the response so that + // releaseStoreProtocol will cleanup later. + synchronized (connectionFailedLock) { + connectionFailed = true; + if (r.isSynthetic()) + forceClose = true; + } + return; + } + } + + /** + * Use the IMAP IDLE command (see + * RFC 2177), + * if supported by the server, to enter idle mode so that the server + * can send unsolicited notifications + * without the need for the client to constantly poll the server. + * Use a ConnectionListener to be notified of + * events. When another thread (e.g., the listener thread) + * needs to issue an IMAP comand for this Store, the idle mode will + * be terminated and this method will return. Typically the caller + * will invoke this method in a loop.

+ * + * If the mail.imap.enableimapevents property is set, notifications + * received while the IDLE command is active will be delivered to + * ConnectionListeners as events with a type of + * IMAPStore.RESPONSE. The event's message will be + * the raw IMAP response string. + * Note that most IMAP servers will not deliver any events when + * using the IDLE command on a connection with no mailbox selected + * (i.e., this method). In most cases you'll want to use the + * idle method on IMAPFolder.

+ * + * NOTE: This capability is highly experimental and likely will change + * in future releases.

+ * + * The mail.imap.minidletime property enforces a minimum delay + * before returning from this method, to ensure that other threads + * have a chance to issue commands before the caller invokes this + * method again. The default delay is 10 milliseconds. + * + * @exception MessagingException if the server doesn't support the + * IDLE extension + * @exception IllegalStateException if the store isn't connected + * + * @since JavaMail 1.4.1 + */ + public void idle() throws MessagingException { + IMAPProtocol p = null; + // ASSERT: Must NOT be called with the connection pool + // synchronization lock held. + assert !Thread.holdsLock(pool); + synchronized (this) { + checkConnected(); + } + boolean needNotification = false; + try { + synchronized (pool) { + p = getStoreProtocol(); + if (pool.idleState != ConnectionPool.RUNNING) { + // some other thread must be running the IDLE + // command, we'll just wait for it to finish + // without aborting it ourselves + try { + // give up lock and wait to be not idle + pool.wait(); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might + // depend on + Thread.currentThread().interrupt(); + // stop waiting and return to caller + throw new MessagingException("idle interrupted", ex); + } + return; + } + p.idleStart(); + needNotification = true; + pool.idleState = ConnectionPool.IDLE; + pool.idleProtocol = p; + } + + /* + * We gave up the pool lock so that other threads + * can get into the pool far enough to see that we're + * in IDLE and abort the IDLE. + * + * Now we read responses from the IDLE command, especially + * including unsolicited notifications from the server. + * We don't hold the pool lock while reading because + * it protects the idleState and other threads need to be + * able to examine the state. + * + * We hold the pool lock while processing the responses. + */ + for (;;) { + Response r = p.readIdleResponse(); + synchronized (pool) { + if (r == null || !p.processIdleResponse(r)) { + pool.idleState = ConnectionPool.RUNNING; + pool.idleProtocol = null; + pool.notifyAll(); + needNotification = false; + break; + } + } + if (enableImapEvents && r.isUnTagged()) { + notifyStoreListeners(IMAPStore.RESPONSE, r.toString()); + } + } + + /* + * Enforce a minimum delay to give time to threads + * processing the responses that came in while we + * were idle. + */ + int minidle = getMinIdleTime(); + if (minidle > 0) { + try { + Thread.sleep(minidle); + } catch (InterruptedException ex) { + // restore the interrupted state, which callers might + // depend on + Thread.currentThread().interrupt(); + } + } + + } catch (BadCommandException bex) { + throw new MessagingException("IDLE not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + if (needNotification) { + synchronized (pool) { + pool.idleState = ConnectionPool.RUNNING; + pool.idleProtocol = null; + pool.notifyAll(); + } + } + releaseStoreProtocol(p); + } + } + + /* + * If an IDLE command is in progress, abort it if necessary, + * and wait until it completes. + * ASSERT: Must be called with the pool's lock held. + */ + private void waitIfIdle() throws ProtocolException { + assert Thread.holdsLock(pool); + while (pool.idleState != ConnectionPool.RUNNING) { + if (pool.idleState == ConnectionPool.IDLE) { + pool.idleProtocol.idleAbort(); + pool.idleState = ConnectionPool.ABORTING; + } + try { + // give up lock and wait to be not idle + pool.wait(); + } catch (InterruptedException ex) { + // If someone is trying to interrupt us we can't keep going + // around the loop waiting for IDLE to complete, but we can't + // just return because callers expect the idleState to be + // RUNNING when we return. Throwing this exception seems + // like the best choice. + throw new ProtocolException("Interrupted waitIfIdle", ex); + } + } + } + + /** + * Send the IMAP ID command (if supported by the server) and return + * the result from the server. The ID command identfies the client + * to the server and returns information about the server to the client. + * See RFC 2971. + * The returned Map is unmodifiable. + * + * @param clientParams a Map of keys and values identifying the client + * @return a Map of keys and values identifying the server + * @exception MessagingException if the server doesn't support the + * ID extension + * @since JavaMail 1.5.1 + */ + public synchronized Map id(Map clientParams) + throws MessagingException { + checkConnected(); + Map serverParams = null; + + IMAPProtocol p = null; + try { + p = getStoreProtocol(); + serverParams = p.id(clientParams); + } catch (BadCommandException bex) { + throw new MessagingException("ID not supported", bex); + } catch (ConnectionException cex) { + throw new StoreClosedException(this, cex.getMessage()); + } catch (ProtocolException pex) { + throw new MessagingException(pex.getMessage(), pex); + } finally { + releaseStoreProtocol(p); + } + return serverParams; + } + + /** + * Handle notifications and alerts. + * Response must be an OK, NO, BAD, or BYE response. + */ + void handleResponseCode(Response r) { + if (enableResponseEvents) + notifyStoreListeners(IMAPStore.RESPONSE, r.toString()); + String s = r.getRest(); // get the text after the response + boolean isAlert = false; + if (s.startsWith("[")) { // a response code + int i = s.indexOf(']'); + // remember if it's an alert + if (i > 0 && s.substring(0, i + 1).equalsIgnoreCase("[ALERT]")) + isAlert = true; + // strip off the response code in any event + s = s.substring(i + 1).trim(); + } + if (isAlert) + notifyStoreListeners(StoreEvent.ALERT, s); + else if (r.isUnTagged() && s.length() > 0) + // Only send notifications that come with untagged + // responses, and only if there is actually some + // text there. + notifyStoreListeners(StoreEvent.NOTICE, s); + } + + private String traceUser(String user) { + return debugusername ? user : ""; + } + + private String tracePassword(String password) { + return debugpassword ? password : + (password == null ? "" : ""); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/IdleManager.java b/app/src/main/java/com/sun/mail/imap/IdleManager.java new file mode 100644 index 0000000000..781f58d5cd --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/IdleManager.java @@ -0,0 +1,491 @@ +/* + * Copyright (c) 2014, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.Socket; +import java.nio.*; +import java.nio.channels.*; +import java.util.*; +import java.util.logging.*; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; + +import javax.mail.*; + +import com.sun.mail.imap.protocol.IMAPProtocol; +import com.sun.mail.util.MailLogger; + +/** + * IdleManager uses the optional IMAP IDLE command + * (RFC 2177) + * to watch multiple folders for new messages. + * IdleManager uses an Executor to execute tasks in separate threads. + * An Executor is typically provided by an ExecutorService. + * For example, for a Java SE application: + *

+ *	ExecutorService es = Executors.newCachedThreadPool();
+ *	final IdleManager idleManager = new IdleManager(session, es);
+ * 
+ * For a Java EE 7 application: + *
+ *	{@literal @}Resource
+ *	ManagedExecutorService es;
+ *	final IdleManager idleManager = new IdleManager(session, es);
+ * 
+ * To watch for new messages in a folder, open the folder, register a listener, + * and ask the IdleManager to watch the folder: + *
+ *	Folder folder = store.getFolder("INBOX");
+ *	folder.open(Folder.READ_WRITE);
+ *	folder.addMessageCountListener(new MessageCountAdapter() {
+ *	    public void messagesAdded(MessageCountEvent ev) {
+ *		Folder folder = (Folder)ev.getSource();
+ *		Message[] msgs = ev.getMessages();
+ *		System.out.println("Folder: " + folder +
+ *		    " got " + msgs.length + " new messages");
+ *		try {
+ *		    // process new messages
+ *		    idleManager.watch(folder); // keep watching for new messages
+ *		} catch (MessagingException mex) {
+ *		    // handle exception related to the Folder
+ *		}
+ *	    }
+ *	});
+ *	idleManager.watch(folder);
+ * 
+ * This delivers the events for each folder in a separate thread, NOT + * using the Executor. To deliver all events in a single thread + * using the Executor, set the following properties for the Session + * (once), and then add listeners and watch the folder as above. + *
+ *	// the following should be done once...
+ *	Properties props = session.getProperties();
+ *	props.put("mail.event.scope", "session"); // or "application"
+ *	props.put("mail.event.executor", es);
+ * 
+ * Note that, after processing new messages in your listener, or doing any + * other operations on the folder in any other thread, you need to tell + * the IdleManager to watch for more new messages. Unless, of course, you + * close the folder. + *

+ * The IdleManager is created with a Session, which it uses only to control + * debug output. A single IdleManager instance can watch multiple Folders + * from multiple Stores and multiple Sessions. + *

+ * Due to limitations in the Java SE nio support, a + * {@link java.nio.channels.SocketChannel SocketChannel} must be used instead + * of a {@link java.net.Socket Socket} to connect to the server. However, + * SocketChannels don't support all the features of Sockets, such as connecting + * through a SOCKS proxy server. SocketChannels also don't support + * simultaneous read and write, which means that the + * {@link com.sun.mail.imap.IMAPFolder#idle idle} method can't be used if + * SocketChannels are being used; use this IdleManager instead. + * To enable support for SocketChannels instead of Sockets, set the + * mail.imap.usesocketchannels property in the Session used to + * access the IMAP Folder. (Or mail.imaps.usesocketchannels if + * you're using the "imaps" protocol.) This will effect all connections in + * that Session, but you can create another Session without this property set + * if you need to use the features that are incompatible with SocketChannels. + *

+ * NOTE: The IdleManager, and all APIs and properties related to it, should + * be considered EXPERIMENTAL. They may be changed in the + * future in ways that are incompatible with applications using the + * current APIs. + * + * @since JavaMail 1.5.2 + */ +public class IdleManager { + private Executor es; + private Selector selector; + private MailLogger logger; + private volatile boolean die = false; + private volatile boolean running; + private Queue toWatch = new ConcurrentLinkedQueue<>(); + private Queue toAbort = new ConcurrentLinkedQueue<>(); + + /** + * Create an IdleManager. The Session is used only to configure + * debugging output. The Executor is used to create the + * "select" thread. + * + * @param session the Session containing configuration information + * @param es the Executor used to create threads + * @exception IOException for Selector failures + */ + public IdleManager(Session session, Executor es) throws IOException { + this.es = es; + logger = new MailLogger(this.getClass(), "DEBUG IMAP", + session.getDebug(), session.getDebugOut()); + selector = Selector.open(); + es.execute(new Runnable() { + @Override + public void run() { + logger.fine("IdleManager select starting"); + try { + running = true; + select(); + } finally { + running = false; + logger.fine("IdleManager select terminating"); + } + } + }); + } + + /** + * Is the IdleManager currently running? The IdleManager starts + * running when the Executor schedules its task. The IdleManager + * stops running after its task detects the stop request from the + * {@link #stop stop} method, or if it terminates abnormally due + * to an unexpected error. + * + * @return true if the IdleMaanger is running + * @since JavaMail 1.5.5 + */ + public boolean isRunning() { + return running; + } + + /** + * Watch the Folder for new messages and other events using the IMAP IDLE + * command. + * + * @param folder the folder to watch + * @exception MessagingException for errors related to the folder + */ + public void watch(Folder folder) + throws MessagingException { + if (die) // XXX - should be IllegalStateException? + throw new MessagingException("IdleManager is not running"); + if (!(folder instanceof IMAPFolder)) + throw new MessagingException("Can only watch IMAP folders"); + IMAPFolder ifolder = (IMAPFolder)folder; + SocketChannel sc = ifolder.getChannel(); + if (sc == null) { + if (folder.isOpen()) + throw new MessagingException( + "Folder is not using SocketChannels"); + else + throw new MessagingException("Folder is not open"); + } + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, "IdleManager watching {0}", + folderName(ifolder)); + // keep trying to start the IDLE command until we're successful. + // may block if we're in the middle of aborting an IDLE command. + int tries = 0; + while (!ifolder.startIdle(this)) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager.watch startIdle failed for {0}", + folderName(ifolder)); + tries++; + } + if (logger.isLoggable(Level.FINEST)) { + if (tries > 0) + logger.log(Level.FINEST, + "IdleManager.watch startIdle succeeded for {0}" + + " after " + tries + " tries", + folderName(ifolder)); + else + logger.log(Level.FINEST, + "IdleManager.watch startIdle succeeded for {0}", + folderName(ifolder)); + } + synchronized (this) { + toWatch.add(ifolder); + selector.wakeup(); + } + } + + /** + * Request that the specified folder abort an IDLE command. + * We can't do the abort directly because the DONE message needs + * to be sent through the (potentially) SSL socket, which means + * we need to be in blocking I/O mode. We can only switch to + * blocking I/O mode when not selecting, so wake up the selector, + * which will process this request when it wakes up. + */ + void requestAbort(IMAPFolder folder) { + toAbort.add(folder); + selector.wakeup(); + } + + /** + * Run the {@link java.nio.channels.Selector#select select} loop + * to poll each watched folder for events sent from the server. + */ + private void select() { + die = false; + try { + while (!die) { + watchAll(); + logger.finest("IdleManager waiting..."); + int ns = selector.select(); + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager selected {0} channels", ns); + if (die || Thread.currentThread().isInterrupted()) + break; + + /* + * Process any selected folders. We cancel the + * selection key for any selected folder, so if we + * need to continue watching that folder it's added + * to the toWatch list again. We can't actually + * register that folder again until the previous + * selection key is cancelled, so we call selectNow() + * just for the side effect of cancelling the selection + * keys. But if selectNow() selects something, we + * process it before adding folders from the toWatch + * queue. And so on until there is nothing to do, at + * which point it's safe to register folders from the + * toWatch queue. This should be "fair" since each + * selection key is used only once before being added + * to the toWatch list. + */ + do { + processKeys(); + } while (selector.selectNow() > 0 || !toAbort.isEmpty()); + } + } catch (InterruptedIOException ex) { + logger.log(Level.FINEST, "IdleManager interrupted", ex); + } catch (IOException ex) { + logger.log(Level.FINEST, "IdleManager got I/O exception", ex); + } catch (Exception ex) { + logger.log(Level.FINEST, "IdleManager got exception", ex); + } finally { + die = true; // prevent new watches in case of exception + logger.finest("IdleManager unwatchAll"); + try { + unwatchAll(); + selector.close(); + } catch (IOException ex2) { + // nothing to do... + logger.log(Level.FINEST, "IdleManager unwatch exception", ex2); + } + logger.fine("IdleManager exiting"); + } + } + + /** + * Register all of the folders in the queue with the selector, + * switching them to nonblocking I/O mode first. + */ + private void watchAll() { + /* + * Pull each of the folders from the toWatch queue + * and register it. + */ + IMAPFolder folder; + while ((folder = toWatch.poll()) != null) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager adding {0} to selector", folderName(folder)); + try { + SocketChannel sc = folder.getChannel(); + if (sc == null) + continue; + // has to be non-blocking to select + sc.configureBlocking(false); + sc.register(selector, SelectionKey.OP_READ, folder); + } catch (IOException ex) { + // oh well, nothing to do + logger.log(Level.FINEST, + "IdleManager can't register folder", ex); + } catch (CancelledKeyException ex) { + // this should never happen + logger.log(Level.FINEST, + "IdleManager can't register folder", ex); + } + } + } + + /** + * Process the selected keys. + */ + private void processKeys() throws IOException { + IMAPFolder folder; + + /* + * First, process any channels with data to read. + */ + Set selectedKeys = selector.selectedKeys(); + /* + * XXX - this is simpler, but it can fail with + * ConcurrentModificationException + * + for (SelectionKey sk : selectedKeys) { + selectedKeys.remove(sk); // only process each key once + ... + } + */ + Iterator it = selectedKeys.iterator(); + while (it.hasNext()) { + SelectionKey sk = it.next(); + it.remove(); // only process each key once + // have to cancel so we can switch back to blocking I/O mode + sk.cancel(); + folder = (IMAPFolder)sk.attachment(); + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager selected folder: {0}", folderName(folder)); + SelectableChannel sc = sk.channel(); + // switch back to blocking to allow normal I/O + sc.configureBlocking(true); + try { + if (folder.handleIdle(false)) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager continue watching folder {0}", + folderName(folder)); + // more to do with this folder, select on it again + toWatch.add(folder); + } else { + // done watching this folder, + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager done watching folder {0}", + folderName(folder)); + } + } catch (MessagingException ex) { + // something went wrong, stop watching this folder + logger.log(Level.FINEST, + "IdleManager got exception for folder: " + + folderName(folder), ex); + } + } + + /* + * Now, process any folders that we need to abort. + */ + while ((folder = toAbort.poll()) != null) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager aborting IDLE for folder: {0}", + folderName(folder)); + SocketChannel sc = folder.getChannel(); + if (sc == null) + continue; + SelectionKey sk = sc.keyFor(selector); + // have to cancel so we can switch back to blocking I/O mode + if (sk != null) + sk.cancel(); + // switch back to blocking to allow normal I/O + sc.configureBlocking(true); + + // if there's a read timeout, have to do the abort in a new thread + Socket sock = sc.socket(); + if (sock != null && sock.getSoTimeout() > 0) { + logger.finest("IdleManager requesting DONE with timeout"); + toWatch.remove(folder); + final IMAPFolder folder0 = folder; + es.execute(new Runnable() { + @Override + public void run() { + // send the DONE and wait for the response + folder0.idleAbortWait(); + } + }); + } else { + folder.idleAbort(); // send the DONE message + // watch for OK response to DONE + // XXX - what if we also added it above? should be a nop + toWatch.add(folder); + } + } + } + + /** + * Stop watching all folders. Cancel any selection keys and, + * most importantly, switch the channel back to blocking mode. + * If there's any folders waiting to be watched, need to abort + * them too. + */ + private void unwatchAll() { + IMAPFolder folder; + Set keys = selector.keys(); + for (SelectionKey sk : keys) { + // have to cancel so we can switch back to blocking I/O mode + sk.cancel(); + folder = (IMAPFolder)sk.attachment(); + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager no longer watching folder: {0}", + folderName(folder)); + SelectableChannel sc = sk.channel(); + // switch back to blocking to allow normal I/O + try { + sc.configureBlocking(true); + folder.idleAbortWait(); // send the DONE message and wait + } catch (IOException ex) { + // ignore it, channel might be closed + logger.log(Level.FINEST, + "IdleManager exception while aborting idle for folder: " + + folderName(folder), ex); + } + } + + /* + * Finally, process any folders waiting to be watched. + */ + while ((folder = toWatch.poll()) != null) { + if (logger.isLoggable(Level.FINEST)) + logger.log(Level.FINEST, + "IdleManager aborting IDLE for unwatched folder: {0}", + folderName(folder)); + SocketChannel sc = folder.getChannel(); + if (sc == null) + continue; + try { + // channel should still be in blocking mode, but make sure + sc.configureBlocking(true); + folder.idleAbortWait(); // send the DONE message and wait + } catch (IOException ex) { + // ignore it, channel might be closed + logger.log(Level.FINEST, + "IdleManager exception while aborting idle for folder: " + + folderName(folder), ex); + } + } + } + + /** + * Stop the IdleManager. The IdleManager can not be restarted. + */ + public synchronized void stop() { + die = true; + logger.fine("IdleManager stopping"); + selector.wakeup(); + } + + /** + * Return the fully qualified name of the folder, for use in log messages. + * Essentially just the getURLName method, but ignoring the + * MessagingException that can never happen. + */ + private static String folderName(Folder folder) { + try { + return folder.getURLName().toString(); + } catch (MessagingException mex) { + // can't happen + return folder.getStore().toString() + "/" + folder.toString(); + } + } +} diff --git a/app/src/main/java/com/sun/mail/imap/MessageCache.java b/app/src/main/java/com/sun/mail/imap/MessageCache.java new file mode 100644 index 0000000000..86c7899aa1 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/MessageCache.java @@ -0,0 +1,443 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.io.PrintStream; +import java.util.*; +import java.util.logging.Level; + +import javax.mail.*; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; + +/** + * A cache of IMAPMessage objects along with the + * mapping from message number to IMAP sequence number. + * + * All operations on this object are protected by the messageCacheLock + * in IMAPFolder. + */ +public class MessageCache { + /* + * The array of IMAPMessage objects. Elements of the array might + * be null if no one has asked for the message. The array expands + * as needed and might be larger than the number of messages in the + * folder. The "size" field indicates the number of entries that + * are valid. + */ + private IMAPMessage[] messages; + + /* + * A parallel array of sequence numbers for each message. If the + * array pointer is null, the sequence number of a message is just + * its message number. This is the common case, until a message is + * expunged. + */ + private int[] seqnums; + + /* + * The amount of the messages (and seqnum) array that is valid. + * Might be less than the actual size of the array. + */ + private int size; + + /** + * The folder these messages belong to. + */ + private IMAPFolder folder; + + // debugging logger + private MailLogger logger; + + /** + * Grow the array by at least this much, to avoid constantly + * reallocating the array. + */ + private static final int SLOP = 64; + + /** + * Construct a new message cache of the indicated size. + */ + MessageCache(IMAPFolder folder, IMAPStore store, int size) { + this.folder = folder; + logger = folder.logger.getSubLogger("messagecache", "DEBUG IMAP MC", + store.getMessageCacheDebug()); + if (logger.isLoggable(Level.CONFIG)) + logger.config("create cache of size " + size); + ensureCapacity(size, 1); + } + + /** + * Constructor for debugging and testing. + */ + MessageCache(int size, boolean debug) { + this.folder = null; + logger = new MailLogger( + this.getClass(), "messagecache", + "DEBUG IMAP MC", debug, System.out); + if (logger.isLoggable(Level.CONFIG)) + logger.config("create DEBUG cache of size " + size); + ensureCapacity(size, 1); + } + + /** + * Size of cache. + * + * @return the size of the cache + */ + public int size() { + return size; + } + + /** + * Get the message object for the indicated message number. + * If the message object hasn't been created, create it. + * + * @param msgnum the message number + * @return the message + */ + public IMAPMessage getMessage(int msgnum) { + // check range + if (msgnum < 1 || msgnum > size) + throw new ArrayIndexOutOfBoundsException( + "message number (" + msgnum + ") out of bounds (" + size + ")"); + IMAPMessage msg = messages[msgnum-1]; + if (msg == null) { + if (logger.isLoggable(Level.FINE)) + logger.fine("create message number " + msgnum); + msg = folder.newIMAPMessage(msgnum); + messages[msgnum-1] = msg; + // mark message expunged if no seqnum + if (seqnumOf(msgnum) <= 0) { + logger.fine("it's expunged!"); + msg.setExpunged(true); + } + } + return msg; + } + + /** + * Get the message object for the indicated sequence number. + * If the message object hasn't been created, create it. + * Return null if there's no message with that sequence number. + * + * @param seqnum the sequence number of the message + * @return the message + */ + public IMAPMessage getMessageBySeqnum(int seqnum) { + int msgnum = msgnumOf(seqnum); + if (msgnum < 0) { // XXX - < 1 ? + if (logger.isLoggable(Level.FINE)) + logger.fine("no message seqnum " + seqnum); + return null; + } else + return getMessage(msgnum); + } + + /** + * Expunge the message with the given sequence number. + * + * @param seqnum the sequence number of the message to expunge + */ + public void expungeMessage(int seqnum) { + int msgnum = msgnumOf(seqnum); + if (msgnum < 0) { + if (logger.isLoggable(Level.FINE)) + logger.fine("expunge no seqnum " + seqnum); + return; // XXX - should never happen + } + IMAPMessage msg = messages[msgnum-1]; + if (msg != null) { + if (logger.isLoggable(Level.FINE)) + logger.fine("expunge existing " + msgnum); + msg.setExpunged(true); + } + if (seqnums == null) { // time to fill it in + logger.fine("create seqnums array"); + seqnums = new int[messages.length]; + for (int i = 1; i < msgnum; i++) + seqnums[i-1] = i; + seqnums[msgnum - 1] = 0; + for (int i = msgnum + 1; i <= seqnums.length; i++) + seqnums[i-1] = i - 1; + } else { + seqnums[msgnum - 1] = 0; + for (int i = msgnum + 1; i <= seqnums.length; i++) { + assert seqnums[i-1] != 1; + if (seqnums[i-1] > 0) + seqnums[i-1]--; + } + } + } + + /** + * Remove all the expunged messages from the array, + * returning a list of removed message objects. + * + * @return the removed messages + */ + public IMAPMessage[] removeExpungedMessages() { + logger.fine("remove expunged messages"); + // list of expunged messages + List mlist = new ArrayList<>(); + + /* + * Walk through the array compressing it by copying + * higher numbered messages further down in the array, + * effectively removing expunged messages from the array. + * oldnum is the index we use to walk through the array. + * newnum is the index where we copy the next valid message. + * oldnum == newnum until we encounter an expunged message. + */ + int oldnum = 1; + int newnum = 1; + while (oldnum <= size) { + // is message expunged? + if (seqnumOf(oldnum) <= 0) { + IMAPMessage m = getMessage(oldnum); + mlist.add(m); + } else { + // keep this message + if (newnum != oldnum) { + // move message down in the array (compact array) + messages[newnum-1] = messages[oldnum-1]; + if (messages[newnum-1] != null) + messages[newnum-1].setMessageNumber(newnum); + } + newnum++; + } + oldnum++; + } + seqnums = null; + shrink(newnum, oldnum); + + IMAPMessage[] rmsgs = new IMAPMessage[mlist.size()]; + if (logger.isLoggable(Level.FINE)) + logger.fine("return " + rmsgs.length); + mlist.toArray(rmsgs); + return rmsgs; + } + + /** + * Remove expunged messages in msgs from the array, + * returning a list of removed message objects. + * All messages in msgs must be IMAPMessage objects + * from this folder. + * + * @param msgs the messages + * @return the removed messages + */ + public IMAPMessage[] removeExpungedMessages(Message[] msgs) { + logger.fine("remove expunged messages"); + // list of expunged messages + List mlist = new ArrayList<>(); + + /* + * Copy the message numbers of the expunged messages into + * a separate array and sort the array to make it easier to + * process later. + */ + int[] mnum = new int[msgs.length]; + for (int i = 0; i < msgs.length; i++) + mnum[i] = msgs[i].getMessageNumber(); + Arrays.sort(mnum); + + /* + * Walk through the array compressing it by copying + * higher numbered messages further down in the array, + * effectively removing expunged messages from the array. + * oldnum is the index we use to walk through the array. + * newnum is the index where we copy the next valid message. + * oldnum == newnum until we encounter an expunged message. + * + * Even though we know the message number of the first possibly + * expunged message, we still start scanning at message number 1 + * so that we can check whether there's any message whose + * sequence number is different than its message number. If there + * is, we can't throw away the seqnums array when we're done. + */ + int oldnum = 1; + int newnum = 1; + int mnumi = 0; // index into mnum + boolean keepSeqnums = false; + while (oldnum <= size) { + /* + * Are there still expunged messsages in msgs to consider, + * and is the message we're considering the next one in the + * list, and is it expunged? + */ + if (mnumi < mnum.length && + oldnum == mnum[mnumi] && + seqnumOf(oldnum) <= 0) { + IMAPMessage m = getMessage(oldnum); + mlist.add(m); + /* + * Just in case there are duplicate entries in the msgs array, + * we keep advancing mnumi past any duplicates, but of course + * stop when we get to the end of the array. + */ + while (mnumi < mnum.length && mnum[mnumi] <= oldnum) + mnumi++; // consider next message in array + } else { + // keep this message + if (newnum != oldnum) { + // move message down in the array (compact array) + messages[newnum-1] = messages[oldnum-1]; + if (messages[newnum-1] != null) + messages[newnum-1].setMessageNumber(newnum); + if (seqnums != null) + seqnums[newnum-1] = seqnums[oldnum-1]; + } + if (seqnums != null && seqnums[newnum-1] != newnum) + keepSeqnums = true; + newnum++; + } + oldnum++; + } + + if (!keepSeqnums) + seqnums = null; + shrink(newnum, oldnum); + + IMAPMessage[] rmsgs = new IMAPMessage[mlist.size()]; + if (logger.isLoggable(Level.FINE)) + logger.fine("return " + rmsgs.length); + mlist.toArray(rmsgs); + return rmsgs; + } + + /** + * Shrink the messages and seqnums arrays. newend is one past last + * valid element. oldend is one past the previous last valid element. + */ + private void shrink(int newend, int oldend) { + size = newend - 1; + if (logger.isLoggable(Level.FINE)) + logger.fine("size now " + size); + if (size == 0) { // no messages left + messages = null; + seqnums = null; + } else if (size > SLOP && size < messages.length / 2) { + // if array shrinks by too much, reallocate it + logger.fine("reallocate array"); + IMAPMessage[] newm = new IMAPMessage[size + SLOP]; + System.arraycopy(messages, 0, newm, 0, size); + messages = newm; + if (seqnums != null) { + int[] news = new int[size + SLOP]; + System.arraycopy(seqnums, 0, news, 0, size); + seqnums = news; + } + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("clean " + newend + " to " + oldend); + // clear out unused entries in array + for (int msgnum = newend; msgnum < oldend; msgnum++) { + messages[msgnum-1] = null; + if (seqnums != null) + seqnums[msgnum-1] = 0; + } + } + } + + /** + * Add count messages to the cache. + * newSeqNum is the sequence number of the first message added. + * + * @param count the number of messges + * @param newSeqNum sequence number of first message + */ + public void addMessages(int count, int newSeqNum) { + if (logger.isLoggable(Level.FINE)) + logger.fine("add " + count + " messages"); + // don't have to do anything other than making sure there's space + ensureCapacity(size + count, newSeqNum); + } + + /* + * Make sure the arrays are at least big enough to hold + * "newsize" messages. + */ + private void ensureCapacity(int newsize, int newSeqNum) { + if (messages == null) + messages = new IMAPMessage[newsize + SLOP]; + else if (messages.length < newsize) { + if (logger.isLoggable(Level.FINE)) + logger.fine("expand capacity to " + newsize); + IMAPMessage[] newm = new IMAPMessage[newsize + SLOP]; + System.arraycopy(messages, 0, newm, 0, messages.length); + messages = newm; + if (seqnums != null) { + int[] news = new int[newsize + SLOP]; + System.arraycopy(seqnums, 0, news, 0, seqnums.length); + for (int i = size; i < news.length; i++) + news[i] = newSeqNum++; + seqnums = news; + if (logger.isLoggable(Level.FINE)) + logger.fine("message " + newsize + + " has sequence number " + seqnums[newsize-1]); + } + } else if (newsize < size) { // shrinking? + // this should never happen + if (logger.isLoggable(Level.FINE)) + logger.fine("shrink capacity to " + newsize); + for (int msgnum = newsize + 1; msgnum <= size; msgnum++) { + messages[msgnum-1] = null; + if (seqnums != null) + seqnums[msgnum-1] = -1; + } + } + size = newsize; + } + + /** + * Return the sequence number for the given message number. + * + * @param msgnum the message number + * @return the sequence number + */ + public int seqnumOf(int msgnum) { + if (seqnums == null) + return msgnum; + else { + if (logger.isLoggable(Level.FINE)) + logger.fine("msgnum " + msgnum + " is seqnum " + + seqnums[msgnum-1]); + return seqnums[msgnum-1]; + } + } + + /** + * Return the message number for the given sequence number. + */ + private int msgnumOf(int seqnum) { + if (seqnums == null) + return seqnum; + if (seqnum < 1) { // should never happen + if (logger.isLoggable(Level.FINE)) + logger.fine("bad seqnum " + seqnum); + return -1; + } + for (int msgnum = seqnum; msgnum <= size; msgnum++) { + if (seqnums[msgnum-1] == seqnum) + return msgnum; + if (seqnums[msgnum-1] > seqnum) + break; // message doesn't exist + } + return -1; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/MessageVanishedEvent.java b/app/src/main/java/com/sun/mail/imap/MessageVanishedEvent.java new file mode 100644 index 0000000000..056cabc68a --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/MessageVanishedEvent.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Folder; +import javax.mail.Message; +import javax.mail.event.MessageCountEvent; + +/** + * This class provides notification of messages that have been removed + * since the folder was last synchronized. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class MessageVanishedEvent extends MessageCountEvent { + + /** + * The message UIDs. + */ + private long[] uids; + + // a reusable empty array + private static final Message[] noMessages = { }; + + private static final long serialVersionUID = 2142028010250024922L; + + /** + * Constructor. + * + * @param folder the containing folder + * @param uids the UIDs for the vanished messages + */ + public MessageVanishedEvent(Folder folder, long[] uids) { + super(folder, REMOVED, true, noMessages); + this.uids = uids; + } + + /** + * Return the UIDs for this event. + * + * @return the UIDs + */ + public long[] getUIDs() { + return uids; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/ModifiedSinceTerm.java b/app/src/main/java/com/sun/mail/imap/ModifiedSinceTerm.java new file mode 100644 index 0000000000..7332266f25 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/ModifiedSinceTerm.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +/** + * Find messages that have been modified since a given MODSEQ value. + * Relies on the server implementing the CONDSTORE extension + * (RFC 4551). + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ +public final class ModifiedSinceTerm extends SearchTerm { + + private long modseq; + + private static final long serialVersionUID = 5151457469634727992L; + + /** + * Constructor. + * + * @param modseq modification sequence number + */ + public ModifiedSinceTerm(long modseq) { + this.modseq = modseq; + } + + /** + * Return the modseq. + * + * @return the modseq + */ + public long getModSeq() { + return modseq; + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * MODSEQ + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + long m; + + try { + if (msg instanceof IMAPMessage) + m = ((IMAPMessage)msg).getModSeq(); + else + return false; + } catch (Exception e) { + return false; + } + + return m >= modseq; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ModifiedSinceTerm)) + return false; + return modseq == ((ModifiedSinceTerm)obj).modseq; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return (int)modseq; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/OlderTerm.java b/app/src/main/java/com/sun/mail/imap/OlderTerm.java new file mode 100644 index 0000000000..19857c4dfd --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/OlderTerm.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +/** + * Find messages that are older than a given interval (in seconds). + * Relies on the server implementing the WITHIN search extension + * (RFC 5032). + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ +public final class OlderTerm extends SearchTerm { + + private int interval; + + private static final long serialVersionUID = 3951078948727995682L; + + /** + * Constructor. + * + * @param interval number of seconds older + */ + public OlderTerm(int interval) { + this.interval = interval; + } + + /** + * Return the interval. + * + * @return the interval + */ + public int getInterval() { + return interval; + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * received date + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Date d; + + try { + d = msg.getReceivedDate(); + } catch (Exception e) { + return false; + } + + if (d == null) + return false; + + return d.getTime() <= + System.currentTimeMillis() - ((long)interval * 1000); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OlderTerm)) + return false; + return interval == ((OlderTerm)obj).interval; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return interval; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/ReferralException.java b/app/src/main/java/com/sun/mail/imap/ReferralException.java new file mode 100644 index 0000000000..f4a335c163 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/ReferralException.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import javax.mail.AuthenticationFailedException; + +/** + * A special kind of AuthenticationFailedException that indicates that + * the reason for the failure was an IMAP REFERRAL in the response code. + * See RFC 2221 for details. + * + * @since JavaMail 1.5.5 + */ + +public class ReferralException extends AuthenticationFailedException { + + private String url; + private String text; + + private static final long serialVersionUID = -3414063558596287683L; + + /** + * Constructs an ReferralException with the specified URL and text. + * + * @param text the detail message + * @param url the URL + */ + public ReferralException(String url, String text) { + super("[REFERRAL " + url + "] " + text); + this.url = url; + this.text = text; + } + + /** + * Return the IMAP URL in the referral. + * + * @return the IMAP URL + */ + public String getUrl() { + return url; + } + + /** + * Return the text sent by the server along with the referral. + * + * @return the text + */ + public String getText() { + return text; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/ResyncData.java b/app/src/main/java/com/sun/mail/imap/ResyncData.java new file mode 100644 index 0000000000..f5c295e426 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/ResyncData.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import com.sun.mail.imap.protocol.UIDSet; + +/** + * Resynchronization data as defined by the QRESYNC extension + * (RFC 5162). + * An instance of ResyncData is supplied to the + * {@link com.sun.mail.imap.IMAPFolder#open(int,com.sun.mail.imap.ResyncData) + * IMAPFolder open} method. + * The CONDSTORE ResyncData instance is used to enable the + * CONDSTORE extension + * (RFC 4551). + * A ResyncData instance with uidvalidity and modseq values + * is used to enable the QRESYNC extension. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class ResyncData { + private long uidvalidity = -1; + private long modseq = -1; + private UIDSet[] uids = null; + + /** + * Used to enable only the CONDSTORE extension. + */ + public static final ResyncData CONDSTORE = new ResyncData(-1, -1); + + /** + * Used to report on changes since the specified modseq. + * If the UIDVALIDITY of the folder has changed, no message + * changes will be reported. The application must check the + * UIDVALIDITY of the folder after open to make sure it's + * the expected folder. + * + * @param uidvalidity the UIDVALIDITY + * @param modseq the MODSEQ + */ + public ResyncData(long uidvalidity, long modseq) { + this.uidvalidity = uidvalidity; + this.modseq = modseq; + this.uids = null; + } + + /** + * Used to limit the reported message changes to those with UIDs + * in the specified range. + * + * @param uidvalidity the UIDVALIDITY + * @param modseq the MODSEQ + * @param uidFirst the first UID + * @param uidLast the last UID + */ + public ResyncData(long uidvalidity, long modseq, + long uidFirst, long uidLast) { + this.uidvalidity = uidvalidity; + this.modseq = modseq; + this.uids = new UIDSet[] { new UIDSet(uidFirst, uidLast) }; + } + + /** + * Used to limit the reported message changes to those with the + * specified UIDs. + * + * @param uidvalidity the UIDVALIDITY + * @param modseq the MODSEQ + * @param uids the UID values + */ + public ResyncData(long uidvalidity, long modseq, long[] uids) { + this.uidvalidity = uidvalidity; + this.modseq = modseq; + this.uids = UIDSet.createUIDSets(uids); + } + + /** + * Get the UIDVALIDITY value specified when this instance was created. + * + * @return the UIDVALIDITY value + */ + public long getUIDValidity() { + return uidvalidity; + } + + /** + * Get the MODSEQ value specified when this instance was created. + * + * @return the MODSEQ value + */ + public long getModSeq() { + return modseq; + } + + /* + * Package private. IMAPProtocol gets this data indirectly + * using Utility.getResyncUIDSet(). + */ + UIDSet[] getUIDSet() { + return uids; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/Rights.java b/app/src/main/java/com/sun/mail/imap/Rights.java new file mode 100644 index 0000000000..d9c4ce0298 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/Rights.java @@ -0,0 +1,444 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.*; + +/** + * The Rights class represents the set of rights for an authentication + * identifier (for instance, a user or a group).

+ * + * A right is represented by the Rights.Right + * inner class.

+ * + * A set of standard rights are predefined (see RFC 2086). Most folder + * implementations are expected to support these rights. Some + * implementations may also support site-defined rights.

+ * + * The following code sample illustrates how to examine your + * rights for a folder. + *

+ *
+ * Rights rights = folder.myRights();
+ *
+ * // Check if I can write this folder
+ * if (rights.contains(Rights.Right.WRITE))
+ *	System.out.println("Can write folder");
+ *
+ * // Now give Joe all my rights, except the ability to write the folder
+ * rights.remove(Rights.Right.WRITE);
+ * ACL acl = new ACL("joe", rights);
+ * folder.setACL(acl);
+ * 
+ *

+ * + * @author Bill Shannon + */ + +public class Rights implements Cloneable { + + private boolean[] rights = new boolean[128]; // XXX + + /** + * This inner class represents an individual right. A set + * of standard rights objects are predefined here. + */ + public static final class Right { + private static Right[] cache = new Right[128]; + + // XXX - initialization order? + /** + * Lookup - mailbox is visible to LIST/LSUB commands. + */ + public static final Right LOOKUP = getInstance('l'); + + /** + * Read - SELECT the mailbox, perform CHECK, FETCH, PARTIAL, + * SEARCH, COPY from mailbox + */ + public static final Right READ = getInstance('r'); + + /** + * Keep seen/unseen information across sessions - STORE \SEEN flag. + */ + public static final Right KEEP_SEEN = getInstance('s'); + + /** + * Write - STORE flags other than \SEEN and \DELETED. + */ + public static final Right WRITE = getInstance('w'); + + /** + * Insert - perform APPEND, COPY into mailbox. + */ + public static final Right INSERT = getInstance('i'); + + /** + * Post - send mail to submission address for mailbox, + * not enforced by IMAP4 itself. + */ + public static final Right POST = getInstance('p'); + + /** + * Create - CREATE new sub-mailboxes in any implementation-defined + * hierarchy, RENAME or DELETE mailbox. + */ + public static final Right CREATE = getInstance('c'); + + /** + * Delete - STORE \DELETED flag, perform EXPUNGE. + */ + public static final Right DELETE = getInstance('d'); + + /** + * Administer - perform SETACL. + */ + public static final Right ADMINISTER = getInstance('a'); + + char right; // the right represented by this Right object + + /** + * Private constructor used only by getInstance. + */ + private Right(char right) { + if ((int)right >= 128) + throw new IllegalArgumentException("Right must be ASCII"); + this.right = right; + } + + /** + * Get a Right object representing the specified character. + * Characters are assigned per RFC 2086. + * + * @param right the character representing the right + * @return the Right object + */ + public static synchronized Right getInstance(char right) { + if ((int)right >= 128) + throw new IllegalArgumentException("Right must be ASCII"); + if (cache[(int)right] == null) + cache[(int)right] = new Right(right); + return cache[(int)right]; + } + + @Override + public String toString() { + return String.valueOf(right); + } + } + + + /** + * Construct an empty Rights object. + */ + public Rights() { } + + /** + * Construct a Rights object initialized with the given rights. + * + * @param rights the rights for initialization + */ + public Rights(Rights rights) { + System.arraycopy(rights.rights, 0, this.rights, 0, this.rights.length); + } + + /** + * Construct a Rights object initialized with the given rights. + * + * @param rights the rights for initialization + */ + public Rights(String rights) { + for (int i = 0; i < rights.length(); i++) + add(Right.getInstance(rights.charAt(i))); + } + + /** + * Construct a Rights object initialized with the given right. + * + * @param right the right for initialization + */ + public Rights(Right right) { + this.rights[(int)right.right] = true; + } + + /** + * Add the specified right to this Rights object. + * + * @param right the right to add + */ + public void add(Right right) { + this.rights[(int)right.right] = true; + } + + /** + * Add all the rights in the given Rights object to this + * Rights object. + * + * @param rights Rights object + */ + public void add(Rights rights) { + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i]) + this.rights[i] = true; + } + + /** + * Remove the specified right from this Rights object. + * + * @param right the right to be removed + */ + public void remove(Right right) { + this.rights[(int)right.right] = false; + } + + /** + * Remove all rights in the given Rights object from this + * Rights object. + * + * @param rights the rights to be removed + */ + public void remove(Rights rights) { + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i]) + this.rights[i] = false; + } + + /** + * Check whether the specified right is present in this Rights object. + * + * @param right the Right to check + * @return true of the given right is present, otherwise false. + */ + public boolean contains(Right right) { + return this.rights[(int)right.right]; + } + + /** + * Check whether all the rights in the specified Rights object are + * present in this Rights object. + * + * @param rights the Rights to check + * @return true if all rights in the given Rights object are present, + * otherwise false. + */ + public boolean contains(Rights rights) { + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i] && !this.rights[i]) + return false; + + // If we've made it till here, return true + return true; + } + + /** + * Check whether the two Rights objects are equal. + * + * @return true if they're equal + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Rights)) + return false; + + Rights rights = (Rights)obj; + + for (int i = 0; i < rights.rights.length; i++) + if (rights.rights[i] != this.rights[i]) + return false; + + return true; + } + + /** + * Compute a hash code for this Rights object. + * + * @return the hash code + */ + @Override + public int hashCode() { + int hash = 0; + for (int i = 0; i < this.rights.length; i++) + if (this.rights[i]) + hash++; + return hash; + } + + /** + * Return all the rights in this Rights object. Returns + * an array of size zero if no rights are set. + * + * @return array of Rights.Right objects representing rights + */ + public Right[] getRights() { + List v = new ArrayList<>(); + for (int i = 0; i < this.rights.length; i++) + if (this.rights[i]) + v.add(Right.getInstance((char)i)); + return v.toArray(new Right[v.size()]); + } + + /** + * Returns a clone of this Rights object. + */ + @Override + public Object clone() { + Rights r = null; + try { + r = (Rights)super.clone(); + r.rights = new boolean[128]; + System.arraycopy(this.rights, 0, r.rights, 0, this.rights.length); + } catch (CloneNotSupportedException cex) { + // ignore, can't happen + } + return r; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < this.rights.length; i++) + if (this.rights[i]) + sb.append((char)i); + return sb.toString(); + } + + /***** + public static void main(String argv[]) throws Exception { + // a new rights object + Rights f1 = new Rights(); + f1.add(Rights.Right.READ); + f1.add(Rights.Right.WRITE); + f1.add(Rights.Right.CREATE); + f1.add(Rights.Right.DELETE); + + // check copy constructor + Rights fc = new Rights(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone + fc = (Rights)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // add a right and make sure it still works right + f1.add(Rights.Right.ADMINISTER); + + // shouldn't be equal here + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone + fc = (Rights)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + fc.add(Rights.Right.INSERT); + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check copy constructor + fc = new Rights(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // another new rights object + Rights f2 = new Rights(Rights.Right.READ); + f2.add(Rights.Right.WRITE); + + if (f1.contains(Rights.Right.READ)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Rights.Right.WRITE)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Rights.Right.CREATE)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Rights.Right.DELETE)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f2.contains(Rights.Right.WRITE)) + System.out.println("success"); + else + System.out.println("fail"); + + + System.out.println("----------------"); + + Right[] r = f1.getRights(); + for (int i = 0; i < r.length; i++) + System.out.println(r[i]); + System.out.println("----------------"); + + if (f1.contains(f2)) // this should be true + System.out.println("success"); + else + System.out.println("fail"); + + if (!f2.contains(f1)) // this should be false + System.out.println("success"); + else + System.out.println("fail"); + + Rights f3 = new Rights(); + f3.add(Rights.Right.READ); + f3.add(Rights.Right.WRITE); + f3.add(Rights.Right.CREATE); + f3.add(Rights.Right.DELETE); + f3.add(Rights.Right.ADMINISTER); + f3.add(Rights.Right.LOOKUP); + + f1.add(Rights.Right.LOOKUP); + + if (f1.equals(f3)) + System.out.println("equals success"); + else + System.out.println("fail"); + if (f3.equals(f1)) + System.out.println("equals success"); + else + System.out.println("fail"); + System.out.println("f1 hash code " + f1.hashCode()); + System.out.println("f3 hash code " + f3.hashCode()); + if (f1.hashCode() == f3.hashCode()) + System.out.println("success"); + else + System.out.println("fail"); + } + ****/ +} diff --git a/app/src/main/java/com/sun/mail/imap/SortTerm.java b/app/src/main/java/com/sun/mail/imap/SortTerm.java new file mode 100644 index 0000000000..6f624e8434 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/SortTerm.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +/** + * A particular sort criteria, as defined by + * RFC 5256. + * Sort criteria are used with the + * {@link IMAPFolder#getSortedMessages getSortedMessages} method. + * Multiple sort criteria are specified in an array with the order in + * the array specifying the order in which the sort criteria are applied. + * + * @since JavaMail 1.4.4 + */ +public final class SortTerm { + /** + * Sort by message arrival date and time. + */ + public static final SortTerm ARRIVAL = new SortTerm("ARRIVAL"); + + /** + * Sort by email address of first Cc recipient. + */ + public static final SortTerm CC = new SortTerm("CC"); + + /** + * Sort by sent date and time. + */ + public static final SortTerm DATE = new SortTerm("DATE"); + + /** + * Sort by first From email address. + */ + public static final SortTerm FROM = new SortTerm("FROM"); + + /** + * Reverse the sort order of the following item. + */ + public static final SortTerm REVERSE = new SortTerm("REVERSE"); + + /** + * Sort by the message size. + */ + public static final SortTerm SIZE = new SortTerm("SIZE"); + + /** + * Sort by the base subject text. Note that the "base subject" + * is defined by RFC 5256 and doesn't include items such as "Re:" + * in the subject header. + */ + public static final SortTerm SUBJECT = new SortTerm("SUBJECT"); + + /** + * Sort by email address of first To recipient. + */ + public static final SortTerm TO = new SortTerm("TO"); + + private String term; + private SortTerm(String term) { + this.term = term; + } + + @Override + public String toString() { + return term; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/Utility.java b/app/src/main/java/com/sun/mail/imap/Utility.java new file mode 100644 index 0000000000..43f19983e3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/Utility.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Arrays; +import java.util.Comparator; + +import javax.mail.*; + +import com.sun.mail.imap.protocol.MessageSet; +import com.sun.mail.imap.protocol.UIDSet; +import java.util.ArrayList; +import java.util.List; + +/** + * Holder for some static utility methods. + * + * @author John Mani + * @author Bill Shannon + */ + +public final class Utility { + + // Cannot be initialized + private Utility() { } + + /** + * Run thru the given array of messages, apply the given + * Condition on each message and generate sets of contiguous + * sequence-numbers for the successful messages. If a message + * in the given array is found to be expunged, it is ignored. + * + * ASSERT: Since this method uses and returns message sequence + * numbers, you should use this method only when holding the + * messageCacheLock. + * + * @param msgs the messages + * @param cond the condition to check + * @return the MessageSet array + */ + public static MessageSet[] toMessageSet(Message[] msgs, Condition cond) { + List v = new ArrayList<>(1); + int current, next; + + IMAPMessage msg; + for (int i = 0; i < msgs.length; i++) { + msg = (IMAPMessage)msgs[i]; + if (msg.isExpunged()) // expunged message, skip it + continue; + + current = msg.getSequenceNumber(); + // Apply the condition. If it fails, skip it. + if ((cond != null) && !cond.test(msg)) + continue; + + MessageSet set = new MessageSet(); + set.start = current; + + // Look for contiguous sequence numbers + for (++i; i < msgs.length; i++) { + // get next message + msg = (IMAPMessage)msgs[i]; + + if (msg.isExpunged()) // expunged message, skip it + continue; + next = msg.getSequenceNumber(); + + // Does this message match our condition ? + if ((cond != null) && !cond.test(msg)) + continue; + + if (next == current+1) + current = next; + else { // break in sequence + // We need to reexamine this message at the top of + // the outer loop, so decrement 'i' to cancel the + // outer loop's autoincrement + i--; + break; + } + } + set.end = current; + v.add(set); + } + + if (v.isEmpty()) // No valid messages + return null; + else { + return v.toArray(new MessageSet[v.size()]); + } + } + /** + * Sort (a copy of) the given array of messages and then + * run thru the sorted array of messages, apply the given + * Condition on each message and generate sets of contiguous + * sequence-numbers for the successful messages. If a message + * in the given array is found to be expunged, it is ignored. + * + * ASSERT: Since this method uses and returns message sequence + * numbers, you should use this method only when holding the + * messageCacheLock. + * + * @param msgs the messages + * @param cond the condition to check + * @return the MessageSet array + * @since JavaMail 1.5.4 + */ + public static MessageSet[] toMessageSetSorted(Message[] msgs, + Condition cond) { + /* + * XXX - This is quick and dirty. A more efficient strategy would be + * to generate an array of message numbers by applying the condition + * (with zero indicating the message doesn't satisfy the condition), + * sort it, and then convert it to a MessageSet skipping all the zeroes. + */ + msgs = msgs.clone(); + Arrays.sort(msgs, + new Comparator() { + @Override + public int compare(Message msg1, Message msg2) { + return msg1.getMessageNumber() - msg2.getMessageNumber(); + } + }); + return toMessageSet(msgs, cond); + } + + /** + * Return UIDSets for the messages. Note that the UIDs + * must have already been fetched for the messages. + * + * @param msgs the messages + * @return the UIDSet array + */ + public static UIDSet[] toUIDSet(Message[] msgs) { + List v = new ArrayList<>(1); + long current, next; + + IMAPMessage msg; + for (int i = 0; i < msgs.length; i++) { + msg = (IMAPMessage)msgs[i]; + if (msg.isExpunged()) // expunged message, skip it + continue; + + current = msg.getUID(); + + UIDSet set = new UIDSet(); + set.start = current; + + // Look for contiguous UIDs + for (++i; i < msgs.length; i++) { + // get next message + msg = (IMAPMessage)msgs[i]; + + if (msg.isExpunged()) // expunged message, skip it + continue; + next = msg.getUID(); + + if (next == current+1) + current = next; + else { // break in sequence + // We need to reexamine this message at the top of + // the outer loop, so decrement 'i' to cancel the + // outer loop's autoincrement + i--; + break; + } + } + set.end = current; + v.add(set); + } + + if (v.isEmpty()) // No valid messages + return null; + else { + return v.toArray(new UIDSet[v.size()]); + } + } + + /** + * Make the ResyncData UIDSet available to IMAPProtocol, + * which is in a different package. Note that this class + * is not included in the public javadocs, thus "hiding" + * this method. + * + * @param rd the ResyncData + * @return the UIDSet array + * @since JavaMail 1.5.1 + */ + public static UIDSet[] getResyncUIDSet(ResyncData rd) { + return rd.getUIDSet(); + } + + /** + * This interface defines the test to be executed in + * toMessageSet(). + */ + public static interface Condition { + public boolean test(IMAPMessage message); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/YoungerTerm.java b/app/src/main/java/com/sun/mail/imap/YoungerTerm.java new file mode 100644 index 0000000000..e8264aed89 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/YoungerTerm.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap; + +import java.util.Date; +import javax.mail.Message; +import javax.mail.search.SearchTerm; + +/** + * Find messages that are younger than a given interval (in seconds). + * Relies on the server implementing the WITHIN search extension + * (RFC 5032). + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ +public final class YoungerTerm extends SearchTerm { + + private int interval; + + private static final long serialVersionUID = 1592714210688163496L; + + /** + * Constructor. + * + * @param interval number of seconds younger + */ + public YoungerTerm(int interval) { + this.interval = interval; + } + + /** + * Return the interval. + * + * @return the interval + */ + public int getInterval() { + return interval; + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * received date + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Date d; + + try { + d = msg.getReceivedDate(); + } catch (Exception e) { + return false; + } + + if (d == null) + return false; + + return d.getTime() >= + System.currentTimeMillis() - ((long)interval * 1000); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof YoungerTerm)) + return false; + return interval == ((YoungerTerm)obj).interval; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return interval; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/package.html b/app/src/main/java/com/sun/mail/imap/package.html new file mode 100644 index 0000000000..1103d26e8d --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/package.html @@ -0,0 +1,1002 @@ + + + + + + +com.sun.mail.imap package + + + +

+An IMAP protocol provider for the Jakarta Mail API +that provides access to an IMAP message store. +Both the IMAP4 and IMAP4rev1 protocols are supported. +Refer to +RFC 3501 +for more information. +The IMAP protocol provider also supports many IMAP extensions (described below). +Note that the server needs to support these extensions (and not all servers do) +in order to use the support in the IMAP provider. +You can query the server for support of these extensions using the +{@link com.sun.mail.imap.IMAPStore#hasCapability IMAPStore hasCapability} +method using the capability name defined by the extension +(see the appropriate RFC) after connecting to the server. +

+UIDPLUS Support +

+The IMAP UIDPLUS extension +(RFC 4315) +is supported via the IMAPFolder methods +{@link com.sun.mail.imap.IMAPFolder#addMessages addMessages}, +{@link com.sun.mail.imap.IMAPFolder#appendUIDMessages appendUIDMessages}, and +{@link com.sun.mail.imap.IMAPFolder#copyUIDMessages copyUIDMessages}. +

+MOVE Support +

+The IMAP MOVE extension +(RFC 6851) +is supported via the IMAPFolder methods +{@link com.sun.mail.imap.IMAPFolder#moveMessages moveMessages} and +{@link com.sun.mail.imap.IMAPFolder#moveUIDMessages moveUIDMessages}. +

+SASL Support +

+The IMAP protocol provider can use SASL +(RFC 4422) +authentication mechanisms on systems that support the +javax.security.sasl APIs. +The SASL-IR +(RFC 4959) +capability is also supported. +In addition to the SASL mechanisms that are built into +the SASL implementation, users can also provide additional +SASL mechanisms of their own design to support custom authentication +schemes. See the + +Java SASL API Programming and Deployment Guide for details. +Note that the current implementation doesn't support SASL mechanisms +that provide their own integrity or confidentiality layer. +

+OAuth 2.0 Support +

+Support for OAuth 2.0 authentication via the + +XOAUTH2 authentication mechanism is provided either through the SASL +support described above or as a built-in authentication mechanism in the +IMAP provider. +The OAuth 2.0 Access Token should be passed as the password for this mechanism. +See +OAuth2 Support for details. +

+Connection Pool +

+A connected IMAPStore maintains a pool of IMAP protocol objects for +use in communicating with the IMAP server. The IMAPStore will create +the initial AUTHENTICATED connection and seed the pool with this +connection. As folders are opened and new IMAP protocol objects are +needed, the IMAPStore will provide them from the connection pool, +or create them if none are available. When a folder is closed, +its IMAP protocol object is returned to the connection pool if the +pool is not over capacity. +

+

+A mechanism is provided for timing out idle connection pool IMAP +protocol objects. Timed out connections are closed and removed (pruned) +from the connection pool. +

+

+The connected IMAPStore object may or may not maintain a separate IMAP +protocol object that provides the store a dedicated connection to the +IMAP server. This is provided mainly for compatibility with previous +implementations of the IMAP protocol provider. +

+QUOTA Support +

+The IMAP QUOTA extension +(RFC 2087) +is supported via the +{@link javax.mail.QuotaAwareStore QuotaAwareStore} interface implemented by +{@link com.sun.mail.imap.IMAPStore IMAPStore}, and the +{@link com.sun.mail.imap.IMAPFolder#getQuota IMAPFolder getQuota} and +{@link com.sun.mail.imap.IMAPFolder#setQuota IMAPFolder setQuota} methods. +ACL Support +

+The IMAP ACL extension +(RFC 2086) +is supported via the +{@link com.sun.mail.imap.Rights Rights} class and the IMAPFolder methods +{@link com.sun.mail.imap.IMAPFolder#getACL getACL}, +{@link com.sun.mail.imap.IMAPFolder#addACL addACL}, +{@link com.sun.mail.imap.IMAPFolder#removeACL removeACL}, +{@link com.sun.mail.imap.IMAPFolder#addRights addRights}, +{@link com.sun.mail.imap.IMAPFolder#removeRights removeRights}, +{@link com.sun.mail.imap.IMAPFolder#listRights listRights}, and +{@link com.sun.mail.imap.IMAPFolder#myRights myRights}. +

+SORT Support +

+The IMAP SORT extension +(RFC 5256) +is supported via the +{@link com.sun.mail.imap.SortTerm SortTerm} class and the IMAPFolder +{@link com.sun.mail.imap.IMAPFolder#getSortedMessages getSortedMessages} +methods. +

+CONDSTORE and QRESYNC Support +

+Basic support is provided for the IMAP CONDSTORE +(RFC 4551) +and QRESYNC +(RFC 5162) +extensions for the purpose of resynchronizing a folder after offline operation. +Of course, the server must support these extensions. +Use of these extensions is enabled by using the new +{@link com.sun.mail.imap.IMAPFolder#open(int,com.sun.mail.imap.ResyncData) +IMAPFolder open} method and supplying an appropriate +{@link com.sun.mail.imap.ResyncData ResyncData} instance. +Using +{@link com.sun.mail.imap.ResyncData#CONDSTORE ResyncData.CONDSTORE} +enables the CONDSTORE extension, which allows you to discover the +modification sequence number (modseq) of messages using the +{@link com.sun.mail.imap.IMAPMessage#getModSeq IMAPMessage getModSeq} +method and the +{@link com.sun.mail.imap.IMAPFolder#getHighestModSeq +IMAPFolder getHighestModSeq} method. +Using a +{@link com.sun.mail.imap.ResyncData ResyncData} instance with appropriate +values also allows the server to report any changes in messages since the last +resynchronization. +The changes are reported as a list of +{@link javax.mail.event.MailEvent MailEvent} instances. +The special +{@link com.sun.mail.imap.MessageVanishedEvent MessageVanishedEvent} reports on +UIDs of messages that have been removed since the last resynchronization. +A +{@link javax.mail.event.MessageChangedEvent MessageChangedEvent} reports on +changes to flags of messages. +For example: +

+
+	Folder folder = store.getFolder("whatever");
+	IMAPFolder ifolder = (IMAPFolder)folder;
+	List<MailEvent> events = ifolder.open(Folder.READ_WRITE,
+		    new ResyncData(prevUidValidity, prevModSeq));
+	for (MailEvent ev : events) {
+	    if (ev instanceOf MessageChangedEvent) {
+		// process flag changes
+	    } else if (ev instanceof MessageVanishedEvent) {
+		// process messages that were removed
+	    }
+	}
+
+

+See the referenced RFCs for more details on these IMAP extensions. +

+WITHIN Search Support +

+The IMAP WITHIN search extension +(RFC 5032) +is supported via the +{@link com.sun.mail.imap.YoungerTerm YoungerTerm} and +{@link com.sun.mail.imap.OlderTerm OlderTerm} +{@link javax.mail.search.SearchTerm SearchTerms}, which can be used as follows: +

+
+	// search for messages delivered in the last day
+	Message[] msgs = folder.search(new YoungerTerm(24 * 60 * 60));
+
+LOGIN-REFERRAL Support +

+The IMAP LOGIN-REFERRAL extension +(RFC 2221) +is supported. +If a login referral is received when connecting or when authentication fails, a +{@link com.sun.mail.imap.ReferralException ReferralException} is thrown. +A referral can also occur when login succeeds. By default, no exception is +thrown in this case. To force an exception to be thrown and the authentication +to fail, set the mail.imap.referralexception property to "true". +

+COMPRESS Support +

+The IMAP COMPRESS extension +(RFC 4978) +is supported. +If the server supports the extension and the +mail.imap.compress.enable property is set to "true", +compression will be enabled. +

+UTF-8 Support +

+The IMAP UTF8 extension +(RFC 6855) +is supported. +If the server supports the extension, the client will enable use of UTF-8, +allowing use of UTF-8 in IMAP protocol strings such as folder names. +

+Properties +

+The IMAP protocol provider supports the following properties, +which may be set in the Jakarta Mail Session object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +

+
+	props.put("mail.imap.port", "888");
+
+

+to set the mail.imap.port property, which is of type int. +

+

+Note that if you're using the "imaps" protocol to access IMAP over SSL, +all the properties would be named "mail.imaps.*". +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IMAP properties
NameTypeDescription
mail.imap.userStringDefault user name for IMAP.
mail.imap.hostStringThe IMAP server to connect to.
mail.imap.portintThe IMAP server port to connect to, if the connect() method doesn't +explicitly specify one. Defaults to 143.
mail.imap.partialfetchbooleanControls whether the IMAP partial-fetch capability should be used. +Defaults to true.
mail.imap.fetchsizeintPartial fetch size in bytes. Defaults to 16K.
mail.imap.peekboolean +If set to true, use the IMAP PEEK option when fetching body parts, +to avoid setting the SEEN flag on messages. +Defaults to false. +Can be overridden on a per-message basis by the +{@link com.sun.mail.imap.IMAPMessage#setPeek setPeek} +method on IMAPMessage. +
mail.imap.ignorebodystructuresizebooleanThe IMAP BODYSTRUCTURE response includes the exact size of each body part. +Normally, this size is used to determine how much data to fetch for each +body part. +Some servers report this size incorrectly in some cases; this property can +be set to work around such server bugs. +If this property is set to true, this size is ignored and data is fetched +until the server reports the end of data. +This will result in an extra fetch if the data size is a multiple of the +block size. +Defaults to false.
mail.imap.connectiontimeoutintSocket connection timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.imap.timeoutintSocket read timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.imap.writetimeoutintSocket write timeout value in milliseconds. +This timeout is implemented by using a +java.util.concurrent.ScheduledExecutorService per connection +that schedules a thread to close the socket if the timeout expires. +Thus, the overhead of using this timeout is one thread per connection. +Default is infinite timeout.
mail.imap.statuscachetimeoutintTimeout value in milliseconds for cache of STATUS command response. +Default is 1000 (1 second). Zero disables cache.
mail.imap.appendbuffersizeint +Maximum size of a message to buffer in memory when appending to an IMAP +folder. If not set, or set to -1, there is no maximum and all messages +are buffered. If set to 0, no messages are buffered. If set to (e.g.) +8192, messages of 8K bytes or less are buffered, larger messages are +not buffered. Buffering saves cpu time at the expense of short term +memory usage. If you commonly append very large messages to IMAP +mailboxes you might want to set this to a moderate value (1M or less). +
mail.imap.connectionpoolsizeintMaximum number of available connections in the connection pool. +Default is 1.
mail.imap.connectionpooltimeoutintTimeout value in milliseconds for connection pool connections. Default +is 45000 (45 seconds).
mail.imap.separatestoreconnectionbooleanFlag to indicate whether to use a dedicated store connection for store +commands. Default is false.
mail.imap.allowreadonlyselectbooleanIf false, attempts to open a folder read/write will fail +if the SELECT command succeeds but indicates that the folder is READ-ONLY. +This sometimes indicates that the folder contents can'tbe changed, but +the flags are per-user and can be changed, such as might be the case for +public shared folders. If true, such open attempts will succeed, allowing +the flags to be changed. The getMode method on the +Folder object will return Folder.READ_ONLY +in this case even though the open method specified +Folder.READ_WRITE. Default is false.
mail.imap.auth.mechanismsString +If set, lists the authentication mechanisms to consider, and the order +in which to consider them. Only mechanisms supported by the server and +supported by the current implementation will be used. +The default is "PLAIN LOGIN NTLM", which includes all +the authentication mechanisms supported by the current implementation +except XOAUTH2. +
mail.imap.auth.login.disablebooleanIf true, prevents use of the non-standard AUTHENTICATE LOGIN +command, instead using the plain LOGIN command. +Default is false.
mail.imap.auth.plain.disablebooleanIf true, prevents use of the AUTHENTICATE PLAIN command. +Default is false.
mail.imap.auth.ntlm.disablebooleanIf true, prevents use of the AUTHENTICATE NTLM command. +Default is false.
mail.imap.auth.ntlm.domainString +The NTLM authentication domain. +
mail.imap.auth.ntlm.flagsint +NTLM protocol-specific flags. +See +http://curl.haxx.se/rfc/ntlm.html#theNtlmFlags for details. +
mail.imap.auth.xoauth2.disablebooleanIf true, prevents use of the AUTHENTICATE XOAUTH2 command. +Because the OAuth 2.0 protocol requires a special access token instead of +a password, this mechanism is disabled by default. Enable it by explicitly +setting this property to "false" or by setting the "mail.imap.auth.mechanisms" +property to "XOAUTH2".
mail.imap.proxyauth.userStringIf the server supports the PROXYAUTH extension, this property +specifies the name of the user to act as. Authenticate to the +server using the administrator's credentials. After authentication, +the IMAP provider will issue the PROXYAUTH command with +the user name specified in this property. +
mail.imap.localaddressString +Local address (host name) to bind to when creating the IMAP socket. +Defaults to the address picked by the Socket class. +Should not normally need to be set, but useful with multi-homed hosts +where it's important to pick a particular local address to bind to. +
mail.imap.localportint +Local port number to bind to when creating the IMAP socket. +Defaults to the port number picked by the Socket class. +
mail.imap.sasl.enableboolean +If set to true, attempt to use the javax.security.sasl package to +choose an authentication mechanism for login. +Defaults to false. +
mail.imap.sasl.mechanismsString +A space or comma separated list of SASL mechanism names to try +to use. +
mail.imap.sasl.authorizationidString +The authorization ID to use in the SASL authentication. +If not set, the authentication ID (user name) is used. +
mail.imap.sasl.realmStringThe realm to use with SASL authentication mechanisms that +require a realm, such as DIGEST-MD5.
mail.imap.sasl.usecanonicalhostnameboolean +If set to true, the canonical host name returned by +{@link java.net.InetAddress#getCanonicalHostName InetAddress.getCanonicalHostName} +is passed to the SASL mechanism, instead of the host name used to connect. +Defaults to false. +
mail.imap.sasl. xgwtrustedapphack.enableboolean +If set to true, enables a workaround for a bug in the Novell Groupwise +XGWTRUSTEDAPP SASL mechanism, when that mechanism is being used. +Defaults to true. +
mail.imap.socketFactorySocketFactory +If set to a class that implements the +javax.net.SocketFactory interface, this class +will be used to create IMAP sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.imap.socketFactory.classString +If set, specifies the name of a class that implements the +javax.net.SocketFactory interface. This class +will be used to create IMAP sockets. +
mail.imap.socketFactory.fallbackboolean +If set to true, failure to create a socket using the specified +socket factory class will cause the socket to be created using +the java.net.Socket class. +Defaults to true. +
mail.imap.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.imap.usesocketchannelsboolean +If set to true, use SocketChannels instead of Sockets for connecting +to the server. Required if using the IdleManager. +Ignored if a socket factory is set. +Defaults to false. +
mail.imap.ssl.enableboolean +If set to true, use SSL to connect and use the SSL port by default. +Defaults to false for the "imap" protocol and true for the "imaps" protocol. +
mail.imap.ssl.checkserveridentityboolean +If set to true, check the server identity as specified by +RFC 2595. +These additional checks based on the content of the server's certificate +are intended to prevent man-in-the-middle attacks. +Defaults to false. +
mail.imap.ssl.trustString +If set, and a socket factory hasn't been specified, enables use of a +{@link com.sun.mail.util.MailSSLSocketFactory MailSSLSocketFactory}. +If set to "*", all hosts are trusted. +If set to a whitespace separated list of hosts, those hosts are trusted. +Otherwise, trust depends on the certificate the server presents. +
mail.imap.ssl.socketFactorySSLSocketFactory +If set to a class that extends the +javax.net.ssl.SSLSocketFactory class, this class +will be used to create IMAP SSL sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.imap.ssl.socketFactory.classString +If set, specifies the name of a class that extends the +javax.net.ssl.SSLSocketFactory class. This class +will be used to create IMAP SSL sockets. +
mail.imap.ssl.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.imap.ssl.protocolsstring +Specifies the SSL protocols that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledProtocols method. +
mail.imap.ssl.ciphersuitesstring +Specifies the SSL cipher suites that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledCipherSuites method. +
mail.imap.starttls.enablebooleanIf true, enables the use of the STARTTLS command (if +supported by the server) to switch the connection to a TLS-protected +connection before issuing any login commands. +If the server does not support STARTTLS, the connection continues without +the use of TLS; see the +mail.imap.starttls.required +property to fail if STARTTLS isn't supported. +Note that an appropriate trust store must configured so that the client +will trust the server's certificate. +Default is false.
mail.imap.starttls.requiredboolean +If true, requires the use of the STARTTLS command. +If the server doesn't support the STARTTLS command, or the command +fails, the connect method will fail. +Defaults to false. +
mail.imap.proxy.hoststring +Specifies the host name of an HTTP web proxy server that will be used for +connections to the mail server. +
mail.imap.proxy.portstring +Specifies the port number for the HTTP web proxy server. +Defaults to port 80. +
mail.imap.proxy.userstring +Specifies the user name to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.imap.proxy.passwordstring +Specifies the password to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.imap.socks.hoststring +Specifies the host name of a SOCKS5 proxy server that will be used for +connections to the mail server. +
mail.imap.socks.portstring +Specifies the port number for the SOCKS5 proxy server. +This should only need to be used if the proxy server is not using +the standard port number of 1080. +
mail.imap.minidletimeint +Applications typically call the idle method in a loop. If another +thread termiantes the IDLE command, it needs a chance to do its +work before another IDLE command is issued. The idle method enforces +a delay to prevent thrashing between the IDLE command and regular +commands. This property sets the delay in milliseconds. If not +set, the default is 10 milliseconds. +
mail.imap.enableresponseeventsboolean +Enable special IMAP-specific events to be delivered to the Store's +ConnectionListener. If true, IMAP OK, NO, BAD, or BYE responses +will be sent as ConnectionEvents with a type of +IMAPStore.RESPONSE. The event's message will be the +raw IMAP response string. +By default, these events are not sent. +NOTE: This capability is highly experimental and likely will change +in future releases. +
mail.imap.enableimapeventsboolean +Enable special IMAP-specific events to be delivered to the Store's +ConnectionListener. If true, unsolicited responses +received during the Store's idle method will be sent +as ConnectionEvents with a type of +IMAPStore.RESPONSE. The event's message will be the +raw IMAP response string. +By default, these events are not sent. +NOTE: This capability is highly experimental and likely will change +in future releases. +
mail.imap.throwsearchexceptionboolean +If set to true and a {@link javax.mail.search.SearchTerm SearchTerm} +passed to the +{@link javax.mail.Folder#search Folder.search} +method is too complex for the IMAP protocol, throw a +{@link javax.mail.search.SearchException SearchException}. +For example, the IMAP protocol only supports less-than and greater-than +comparisons for a {@link javax.mail.search.SizeTerm SizeTerm}. +If false, the search will be done locally by fetching the required +message data and comparing it locally. +Defaults to false. +
mail.imap.folder.classString +Class name of a subclass of com.sun.mail.imap.IMAPFolder. +The subclass can be used to provide support for additional IMAP commands. +The subclass must have public constructors of the form +public MyIMAPFolder(String fullName, char separator, IMAPStore store, +Boolean isNamespace) and +public MyIMAPFolder(ListInfo li, IMAPStore store) +
mail.imap.closefoldersonstorefailureboolean +In some cases, a failure of the Store connection indicates a failure of the +server, and all Folders associated with that Store should also be closed. +In other cases, a Store connection failure may be a transient failure, and +Folders may continue to operate normally. +If this property is true (the default), failures in the Store connection cause +all associated Folders to be closed. +Set this property to false to better handle transient failures in the Store +connection. +
mail.imap.finalizecleancloseboolean +When the finalizer for IMAPStore is called, +should the connection to the server be closed cleanly, as if the +application called the close method? +Or should the connection to the server be closed without sending +any commands to the server? +Defaults to false, the connection is not closed cleanly. +
mail.imap.referralexceptionboolean +If set to true and an IMAP login referral is returned when the authentication +succeeds, fail the connect request and throw a +{@link com.sun.mail.imap.ReferralException ReferralException}. +Defaults to false. +
mail.imap.compress.enableboolean +If set to true and the IMAP server supports the COMPRESS=DEFLATE extension, +compression will be enabled. +Defaults to false. +
mail.imap.compress.levelint +The compression level to be used, in the range -1 to 9. +See the {@link java.util.zip.Deflater Deflater} class for details. +
mail.imap.compress.strategyint +The compression strategy to be used, in the range 0 to 2. +See the {@link java.util.zip.Deflater Deflater} class for details. +
mail.imap.reusetagprefixboolean +If true, always use "A" for the IMAP command tag prefix. +If false, the IMAP command tag prefix is different for each connection, +from "A" through "ZZZ" and then wrapping around to "A". +Applications should never need to set this. +Defaults to false. +
+

+In general, applications should not need to use the classes in this +package directly. Instead, they should use the APIs defined by the +javax.mail package (and subpackages). Applications should +never construct instances of IMAPStore or +IMAPFolder directly. Instead, they should use the +Session method getStore to acquire an +appropriate Store object, and from that acquire +Folder objects. +

+Loggers +

+In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, +the com.sun.mail.imap provider logs the same information using +{@link java.util.logging.Logger} as described in the following table: +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
IMAP Loggers
Logger NameLogging LevelPurpose
com.sun.mail.imapCONFIGConfiguration of the IMAPStore
com.sun.mail.imapFINEGeneral debugging output
com.sun.mail.imap.connectionpoolCONFIGConfiguration of the IMAP connection pool
com.sun.mail.imap.connectionpoolFINEDebugging output related to the IMAP connection pool
com.sun.mail.imap.messagecacheCONFIGConfiguration of the IMAP message cache
com.sun.mail.imap.messagecacheFINEDebugging output related to the IMAP message cache
com.sun.mail.imap.protocolFINESTComplete protocol trace
+ +WARNING +

+WARNING: The APIs unique to this package should be +considered EXPERIMENTAL. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +

+ + + diff --git a/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxDecoder.java b/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxDecoder.java new file mode 100644 index 0000000000..af35020e9c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxDecoder.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.text.StringCharacterIterator; +import java.text.CharacterIterator; + +/** + * See the BASE64MailboxEncoder for a description of the RFC2060 and how + * mailbox names should be encoded. This class will do the correct decoding + * for mailbox names. + * + * @author Christopher Cotton + */ + +public class BASE64MailboxDecoder { + + public static String decode(String original) { + if (original == null || original.length() == 0) + return original; + + boolean changedString = false; + int copyTo = 0; + // it will always be less than the original + char[] chars = new char[original.length()]; + StringCharacterIterator iter = new StringCharacterIterator(original); + + for(char c = iter.first(); c != CharacterIterator.DONE; + c = iter.next()) { + + if (c == '&') { + changedString = true; + copyTo = base64decode(chars, copyTo, iter); + } else { + chars[copyTo++] = c; + } + } + + // now create our string from the char array + if (changedString) { + return new String(chars, 0, copyTo); + } else { + return original; + } + } + + + protected static int base64decode(char[] buffer, int offset, + CharacterIterator iter) { + boolean firsttime = true; + int leftover = -1; + + while(true) { + // get the first byte + byte orig_0 = (byte) iter.next(); + if (orig_0 == -1) break; // no more chars + if (orig_0 == '-') { + if (firsttime) { + // means we got the string "&-" which is turned into a "&" + buffer[offset++] = '&'; + } + // we are done now + break; + } + firsttime = false; + + // next byte + byte orig_1 = (byte) iter.next(); + if (orig_1 == -1 || orig_1 == '-') + break; // no more chars, invalid base64 + + byte a, b, current; + a = pem_convert_array[orig_0 & 0xff]; + b = pem_convert_array[orig_1 & 0xff]; + // The first decoded byte + current = (byte)(((a << 2) & 0xfc) | ((b >>> 4) & 3)); + + // use the leftover to create a Unicode Character (2 bytes) + if (leftover != -1) { + buffer[offset++] = (char)(leftover << 8 | (current & 0xff)); + leftover = -1; + } else { + leftover = current & 0xff; + } + + byte orig_2 = (byte) iter.next(); + if (orig_2 == '=') { // End of this BASE64 encoding + continue; + } else if (orig_2 == -1 || orig_2 == '-') { + break; // no more chars + } + + // second decoded byte + a = b; + b = pem_convert_array[orig_2 & 0xff]; + current = (byte)(((a << 4) & 0xf0) | ((b >>> 2) & 0xf)); + + // use the leftover to create a Unicode Character (2 bytes) + if (leftover != -1) { + buffer[offset++] = (char)(leftover << 8 | (current & 0xff)); + leftover = -1; + } else { + leftover = current & 0xff; + } + + byte orig_3 = (byte) iter.next(); + if (orig_3 == '=') { // End of this BASE64 encoding + continue; + } else if (orig_3 == -1 || orig_3 == '-') { + break; // no more chars + } + + // The third decoded byte + a = b; + b = pem_convert_array[orig_3 & 0xff]; + current = (byte)(((a << 6) & 0xc0) | (b & 0x3f)); + + // use the leftover to create a Unicode Character (2 bytes) + if (leftover != -1) { + buffer[offset++] = (char)(leftover << 8 | (current & 0xff)); + leftover = -1; + } else { + leftover = current & 0xff; + } + } + + return offset; + } + + /** + * This character array provides the character to value map + * based on RFC1521, but with the modification from RFC2060 + * which changes the '/' to a ','. + */ + + // shared with BASE64MailboxEncoder + static final char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+',',' // 7 + }; + + private static final byte pem_convert_array[] = new byte[256]; + + static { + for (int i = 0; i < 255; i++) + pem_convert_array[i] = -1; + for(int i = 0; i < pem_array.length; i++) + pem_convert_array[pem_array[i]] = (byte) i; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxEncoder.java b/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxEncoder.java new file mode 100644 index 0000000000..1013baf021 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/BASE64MailboxEncoder.java @@ -0,0 +1,230 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; + + +/** + * From RFC2060: + * + *
+ *
+ * 5.1.3.  Mailbox International Naming Convention
+ *
+ *   By convention, international mailbox names are specified using a
+ *   modified version of the UTF-7 encoding described in [UTF-7].  The
+ *   purpose of these modifications is to correct the following problems
+ *   with UTF-7:
+ *
+ *      1) UTF-7 uses the "+" character for shifting; this conflicts with
+ *         the common use of "+" in mailbox names, in particular USENET
+ *         newsgroup names.
+ *
+ *      2) UTF-7's encoding is BASE64 which uses the "/" character; this
+ *         conflicts with the use of "/" as a popular hierarchy delimiter.
+ *
+ *      3) UTF-7 prohibits the unencoded usage of "\"; this conflicts with
+ *         the use of "\" as a popular hierarchy delimiter.
+ *
+ *      4) UTF-7 prohibits the unencoded usage of "~"; this conflicts with
+ *         the use of "~" in some servers as a home directory indicator.
+ *
+ *      5) UTF-7 permits multiple alternate forms to represent the same
+ *         string; in particular, printable US-ASCII chararacters can be
+ *         represented in encoded form.
+ *
+ *   In modified UTF-7, printable US-ASCII characters except for "&"
+ *   represent themselves; that is, characters with octet values 0x20-0x25
+ *   and 0x27-0x7e.  The character "&" (0x26) is represented by the two-
+ *   octet sequence "&-".
+ *
+ *   All other characters (octet values 0x00-0x1f, 0x7f-0xff, and all
+ *   Unicode 16-bit octets) are represented in modified BASE64, with a
+ *   further modification from [UTF-7] that "," is used instead of "/".
+ *   Modified BASE64 MUST NOT be used to represent any printing US-ASCII
+ *   character which can represent itself.
+ *
+ *   "&" is used to shift to modified BASE64 and "-" to shift back to US-
+ *   ASCII.  All names start in US-ASCII, and MUST end in US-ASCII (that
+ *   is, a name that ends with a Unicode 16-bit octet MUST end with a "-
+ *   ").
+ *
+ *   For example, here is a mailbox name which mixes English, Japanese,
+ *   and Chinese text: ~peter/mail/&ZeVnLIqe-/&U,BTFw-
+ *
+ * 
+ * + * This class will do the correct Encoding for the IMAP mailboxes. + * + * @author Christopher Cotton + */ + +public class BASE64MailboxEncoder { + protected byte[] buffer = new byte[4]; + protected int bufsize = 0; + protected boolean started = false; + protected Writer out = null; + + + public static String encode(String original) { + BASE64MailboxEncoder base64stream = null; + char origchars[] = original.toCharArray(); + int length = origchars.length; + boolean changedString = false; + CharArrayWriter writer = new CharArrayWriter(length); + + // loop over all the chars + for(int index = 0; index < length; index++) { + char current = origchars[index]; + + // octets in the range 0x20-0x25,0x27-0x7e are themselves + // 0x26 "&" is represented as "&-" + if (current >= 0x20 && current <= 0x7e) { + if (base64stream != null) { + base64stream.flush(); + } + + if (current == '&') { + changedString = true; + writer.write('&'); + writer.write('-'); + } else { + writer.write(current); + } + } else { + + // use a B64MailboxEncoder to write out the other bytes + // as a modified BASE64. The stream will write out + // the beginning '&' and the ending '-' which is part + // of every encoding. + + if (base64stream == null) { + base64stream = new BASE64MailboxEncoder(writer); + changedString = true; + } + + base64stream.write(current); + } + } + + + if (base64stream != null) { + base64stream.flush(); + } + + if (changedString) { + return writer.toString(); + } else { + return original; + } + } + + + /** + * Create a BASE64 encoder + * + * @param what where to write the encoded name + */ + public BASE64MailboxEncoder(Writer what) { + out = what; + } + + public void write(int c) { + try { + // write out the initial character if this is the first time + if (!started) { + started = true; + out.write('&'); + } + + // we write each character as a 2 byte unicode character + buffer[bufsize++] = (byte) (c >> 8); + buffer[bufsize++] = (byte) (c & 0xff); + + if (bufsize >= 3) { + encode(); + bufsize -= 3; + } + } catch (IOException e) { + //e.printStackTrace(); + } + } + + + public void flush() { + try { + // flush any bytes we have + if (bufsize > 0) { + encode(); + bufsize = 0; + } + + // write the terminating character of the encoding + if (started) { + out.write('-'); + started = false; + } + } catch (IOException e) { + //e.printStackTrace(); + } + } + + + protected void encode() throws IOException { + byte a, b, c; + if (bufsize == 1) { + a = buffer[0]; + b = 0; + c = 0; + out.write(pem_array[(a >>> 2) & 0x3F]); + out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + // no padding characters are written + } else if (bufsize == 2) { + a = buffer[0]; + b = buffer[1]; + c = 0; + out.write(pem_array[(a >>> 2) & 0x3F]); + out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + out.write(pem_array[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + // no padding characters are written + } else { + a = buffer[0]; + b = buffer[1]; + c = buffer[2]; + out.write(pem_array[(a >>> 2) & 0x3F]); + out.write(pem_array[((a << 4) & 0x30) + ((b >>> 4) & 0xf)]); + out.write(pem_array[((b << 2) & 0x3c) + ((c >>> 6) & 0x3)]); + out.write(pem_array[c & 0x3F]); + + // copy back the extra byte + if (bufsize == 4) + buffer[0] = buffer[3]; + } + } + + private final static char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+',',' // 7 + }; +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/BODY.java b/app/src/main/java/com/sun/mail/imap/protocol/BODY.java new file mode 100644 index 0000000000..5f04edc2c5 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/BODY.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.ByteArrayInputStream; +import com.sun.mail.iap.*; +import com.sun.mail.util.ASCIIUtility; + +/** + * The BODY fetch response item. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BODY implements Item { + + static final char[] name = {'B','O','D','Y'}; + + private final int msgno; + private final ByteArray data; + private final String section; + private final int origin; + private final boolean isHeader; + + /** + * Constructor + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public BODY(FetchResponse r) throws ParsingException { + msgno = r.getNumber(); + + r.skipSpaces(); + + if (r.readByte() != '[') + throw new ParsingException( + "BODY parse error: missing ``['' at section start"); + section = r.readString(']'); + if (r.readByte() != ']') + throw new ParsingException( + "BODY parse error: missing ``]'' at section end"); + isHeader = section.regionMatches(true, 0, "HEADER", 0, 6); + + if (r.readByte() == '<') { // origin + origin = r.readNumber(); + r.skip(1); // skip '>'; + } else + origin = -1; + + data = r.readByteArray(); + } + + public ByteArray getByteArray() { + return data; + } + + public ByteArrayInputStream getByteArrayInputStream() { + if (data != null) + return data.toByteArrayInputStream(); + else + return null; + } + + public boolean isHeader() { + return isHeader; + } + + public String getSection() { + return section; + } + + /** + * @since Jakarta Mail 1.6.4 + */ + public int getOrigin() { + return origin; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/BODYSTRUCTURE.java b/app/src/main/java/com/sun/mail/imap/protocol/BODYSTRUCTURE.java new file mode 100644 index 0000000000..3a5945cc0e --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/BODYSTRUCTURE.java @@ -0,0 +1,450 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; +import javax.mail.internet.ParameterList; +import com.sun.mail.iap.*; +import com.sun.mail.util.PropUtil; + +/** + * A BODYSTRUCTURE response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BODYSTRUCTURE implements Item { + + static final char[] name = + {'B','O','D','Y','S','T','R','U','C','T','U','R','E'}; + public int msgno; + + public String type; // Type + public String subtype; // Subtype + public String encoding; // Encoding + public int lines = -1; // Size in lines + public int size = -1; // Size in bytes + public String disposition; // Disposition + public String id; // Content-ID + public String description; // Content-Description + public String md5; // MD-5 checksum + public String attachment; // Attachment name + public ParameterList cParams; // Body parameters + public ParameterList dParams; // Disposition parameters + public String[] language; // Language + public BODYSTRUCTURE[] bodies; // array of BODYSTRUCTURE objects + // for multipart & message/rfc822 + public ENVELOPE envelope; // for message/rfc822 + + private static int SINGLE = 1; + private static int MULTI = 2; + private static int NESTED = 3; + private int processedType; // MULTI | SINGLE | NESTED + + // special debugging output to debug parsing errors + private static final boolean parseDebug = + PropUtil.getBooleanSystemProperty("mail.imap.parse.debug", false); + + + public BODYSTRUCTURE(FetchResponse r) throws ParsingException { + if (parseDebug) + System.out.println("DEBUG IMAP: parsing BODYSTRUCTURE"); + msgno = r.getNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: msgno " + msgno); + + r.skipSpaces(); + + if (r.readByte() != '(') + throw new ParsingException( + "BODYSTRUCTURE parse error: missing ``('' at start"); + + if (r.peekByte() == '(') { // multipart + if (parseDebug) + System.out.println("DEBUG IMAP: parsing multipart"); + type = "multipart"; + processedType = MULTI; + List v = new ArrayList<>(1); + int i = 1; + do { + v.add(new BODYSTRUCTURE(r)); + /* + * Even though the IMAP spec says there can't be any spaces + * between parts, some servers erroneously put a space in + * here. In the spirit of "be liberal in what you accept", + * we skip it. + */ + r.skipSpaces(); + } while (r.peekByte() == '('); + + // setup bodies. + bodies = v.toArray(new BODYSTRUCTURE[v.size()]); + + subtype = r.readString(); // subtype + if (parseDebug) + System.out.println("DEBUG IMAP: subtype " + subtype); + + if (r.isNextNonSpace(')')) { // done + if (parseDebug) + System.out.println("DEBUG IMAP: parse DONE"); + return; + } + + // Else, we have extension data + + if (parseDebug) + System.out.println("DEBUG IMAP: parsing extension data"); + // Body parameters + cParams = parseParameters(r); + if (r.isNextNonSpace(')')) { // done + if (parseDebug) + System.out.println("DEBUG IMAP: body parameters DONE"); + return; + } + + // Disposition + byte b = r.peekByte(); + if (b == '(') { + if (parseDebug) + System.out.println("DEBUG IMAP: parse disposition"); + r.readByte(); + disposition = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: disposition " + + disposition); + dParams = parseParameters(r); + if (!r.isNextNonSpace(')')) // eat the end ')' + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + "missing ``)'' at end of disposition in multipart"); + if (parseDebug) + System.out.println("DEBUG IMAP: disposition DONE"); + } else if (b == 'N' || b == 'n') { + if (parseDebug) + System.out.println("DEBUG IMAP: disposition NIL"); + r.skip(3); // skip 'NIL' + } else { + /* + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + type + "/" + subtype + ": " + + "bad multipart disposition, b " + b); + */ + if (parseDebug) + System.out.println("DEBUG IMAP: bad multipart disposition" + + ", applying Exchange bug workaround"); + description = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: multipart description " + + description); + // Throw away whatever comes after it, since we have no + // idea what it's supposed to be + while (r.readByte() == ' ') + parseBodyExtension(r); + return; + } + + // RFC3501 allows no body-fld-lang after body-fld-disp, + // even though RFC2060 required it + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: no body-fld-lang"); + return; // done + } + + // Language + if (r.peekByte() == '(') { // a list follows + language = r.readStringList(); + if (parseDebug) + System.out.println( + "DEBUG IMAP: language len " + language.length); + } else { + String l = r.readString(); + if (l != null) { + String[] la = { l }; + language = la; + if (parseDebug) + System.out.println("DEBUG IMAP: language " + l); + } + } + + // RFC3501 defines an optional "body location" next, + // but for now we ignore it along with other extensions. + + // Throw away any further extension data + while (r.readByte() == ' ') + parseBodyExtension(r); + } else if (r.peekByte() == ')') { // (illegal) empty body + /* + * Domino will fail to return the body structure of nested messages. + * Fake it by providing an empty message. Could probably do better + * with more work... + */ + /* + * XXX - this prevents the exception, but without the exception + * the application has no way to know the data from the message + * is missing. + * + if (parseDebug) + System.out.println("DEBUG IMAP: empty body, fake it"); + r.readByte(); + type = "text"; + subtype = "plain"; + lines = 0; + size = 0; + */ + throw new ParsingException( + "BODYSTRUCTURE parse error: missing body content"); + } else { // Single part + if (parseDebug) + System.out.println("DEBUG IMAP: single part"); + type = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: type " + type); + processedType = SINGLE; + subtype = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: subtype " + subtype); + + // SIMS 4.0 returns NIL for a Content-Type of "binary", fix it here + if (type == null) { + type = "application"; + subtype = "octet-stream"; + } + cParams = parseParameters(r); + if (parseDebug) + System.out.println("DEBUG IMAP: cParams " + cParams); + id = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: id " + id); + description = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: description " + description); + /* + * XXX - Work around bug in Exchange 2010 that + * returns unquoted string. + */ + encoding = r.readAtomString(); + if (encoding != null && encoding.equalsIgnoreCase("NIL")) { + if (parseDebug) + System.out.println("DEBUG IMAP: NIL encoding" + + ", applying Exchange bug workaround"); + encoding = null; + } + /* + * XXX - Work around bug in office365.com that returns + * a string with a trailing space in some cases. + */ + if (encoding != null) + encoding = encoding.trim(); + if (parseDebug) + System.out.println("DEBUG IMAP: encoding " + encoding); + size = r.readNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: size " + size); + if (size < 0) + throw new ParsingException( + "BODYSTRUCTURE parse error: bad ``size'' element"); + + // "text/*" & "message/rfc822" types have additional data .. + if (type.equalsIgnoreCase("text")) { + lines = r.readNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: lines " + lines); + if (lines < 0) + throw new ParsingException( + "BODYSTRUCTURE parse error: bad ``lines'' element"); + } else if (type.equalsIgnoreCase("message") && + subtype.equalsIgnoreCase("rfc822")) { + // Nested message + processedType = NESTED; + // The envelope comes next, but sadly Gmail handles nested + // messages just like simple body parts and fails to return + // the envelope and body structure of the message (sort of + // like IMAP4 before rev1). + r.skipSpaces(); + if (r.peekByte() == '(') { // the envelope follows + envelope = new ENVELOPE(r); + if (parseDebug) + System.out.println( + "DEBUG IMAP: got envelope of nested message"); + BODYSTRUCTURE[] bs = { new BODYSTRUCTURE(r) }; + bodies = bs; + lines = r.readNumber(); + if (parseDebug) + System.out.println("DEBUG IMAP: lines " + lines); + if (lines < 0) + throw new ParsingException( + "BODYSTRUCTURE parse error: bad ``lines'' element"); + } else { + if (parseDebug) + System.out.println("DEBUG IMAP: " + + "missing envelope and body of nested message"); + } + } else { + // Detect common error of including lines element on other types + r.skipSpaces(); + byte bn = r.peekByte(); + if (Character.isDigit((char)bn)) // number + throw new ParsingException( + "BODYSTRUCTURE parse error: server erroneously " + + "included ``lines'' element with type " + + type + "/" + subtype); + } + + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: parse DONE"); + return; // done + } + + // Optional extension data + + // MD5 + md5 = r.readString(); + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: no MD5 DONE"); + return; // done + } + + // Disposition + byte b = r.readByte(); + if (b == '(') { + disposition = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: disposition " + + disposition); + dParams = parseParameters(r); + if (parseDebug) + System.out.println("DEBUG IMAP: dParams " + dParams); + if (!r.isNextNonSpace(')')) // eat the end ')' + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + "missing ``)'' at end of disposition"); + } else if (b == 'N' || b == 'n') { + if (parseDebug) + System.out.println("DEBUG IMAP: disposition NIL"); + r.skip(2); // skip 'NIL' + } else { + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + type + "/" + subtype + ": " + + "bad single part disposition, b " + b); + } + + if (r.isNextNonSpace(')')) { + if (parseDebug) + System.out.println("DEBUG IMAP: disposition DONE"); + return; // done + } + + // Language + if (r.peekByte() == '(') { // a list follows + language = r.readStringList(); + if (parseDebug) + System.out.println("DEBUG IMAP: language len " + + language.length); + } else { // protocol is unnessarily complex here + String l = r.readString(); + if (l != null) { + String[] la = { l }; + language = la; + if (parseDebug) + System.out.println("DEBUG IMAP: language " + l); + } + } + + // RFC3501 defines an optional "body location" next, + // but for now we ignore it along with other extensions. + + // Throw away any further extension data + while (r.readByte() == ' ') + parseBodyExtension(r); + if (parseDebug) + System.out.println("DEBUG IMAP: all DONE"); + } + } + + public boolean isMulti() { + return processedType == MULTI; + } + + public boolean isSingle() { + return processedType == SINGLE; + } + + public boolean isNested() { + return processedType == NESTED; + } + + private ParameterList parseParameters(Response r) + throws ParsingException { + r.skipSpaces(); + + ParameterList list = null; + byte b = r.readByte(); + if (b == '(') { + list = new ParameterList(); + do { + String name = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: parameter name " + name); + if (name == null) + throw new ParsingException( + "BODYSTRUCTURE parse error: " + + type + "/" + subtype + ": " + + "null name in parameter list"); + String value = r.readString(); + if (parseDebug) + System.out.println("DEBUG IMAP: parameter value " + value); + if (value == null) { // work around buggy servers + if (parseDebug) + System.out.println("DEBUG IMAP: NIL parameter value" + + ", applying Exchange bug workaround"); + value = ""; + } + list.set(name, value); + } while (!r.isNextNonSpace(')')); + list.combineSegments(); + } else if (b == 'N' || b == 'n') { + if (parseDebug) + System.out.println("DEBUG IMAP: parameter list NIL"); + r.skip(2); + } else + throw new ParsingException("Parameter list parse error"); + + return list; + } + + private void parseBodyExtension(Response r) throws ParsingException { + r.skipSpaces(); + + byte b = r.peekByte(); + if (b == '(') { + r.skip(1); // skip '(' + do { + parseBodyExtension(r); + } while (!r.isNextNonSpace(')')); + } else if (Character.isDigit((char)b)) // number + r.readNumber(); + else // nstring + r.readString(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/ENVELOPE.java b/app/src/main/java/com/sun/mail/imap/protocol/ENVELOPE.java new file mode 100644 index 0000000000..eeafd143e2 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/ENVELOPE.java @@ -0,0 +1,224 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; +import java.util.Date; +import java.io.UnsupportedEncodingException; +import java.text.ParseException; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.AddressException; +import javax.mail.internet.MailDateFormat; +import javax.mail.internet.MimeUtility; +import com.sun.mail.iap.*; +import com.sun.mail.util.PropUtil; + +/** + * The ENEVELOPE item of an IMAP FETCH response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class ENVELOPE implements Item { + + // IMAP item name + static final char[] name = {'E','N','V','E','L','O','P','E'}; + public int msgno; + + public Date date = null; + public String subject; + public InternetAddress[] from; + public InternetAddress[] sender; + public InternetAddress[] replyTo; + public InternetAddress[] to; + public InternetAddress[] cc; + public InternetAddress[] bcc; + public String inReplyTo; + public String messageId; + + // Used to parse dates + private static final MailDateFormat mailDateFormat = new MailDateFormat(); + + // special debugging output to debug parsing errors + private static final boolean parseDebug = + PropUtil.getBooleanSystemProperty("mail.imap.parse.debug", false); + + public ENVELOPE(FetchResponse r) throws ParsingException { + if (parseDebug) + System.out.println("parse ENVELOPE"); + msgno = r.getNumber(); + + r.skipSpaces(); + + if (r.readByte() != '(') + throw new ParsingException("ENVELOPE parse error"); + + String s = r.readString(); + if (s != null) { + try { + synchronized (mailDateFormat) { + date = mailDateFormat.parse(s); + } + } catch (ParseException pex) { + } + } + if (parseDebug) + System.out.println(" Date: " + date); + + subject = r.readString(); + if (parseDebug) + System.out.println(" Subject: " + subject); + if (parseDebug) + System.out.println(" From addresses:"); + from = parseAddressList(r); + if (parseDebug) + System.out.println(" Sender addresses:"); + sender = parseAddressList(r); + if (parseDebug) + System.out.println(" Reply-To addresses:"); + replyTo = parseAddressList(r); + if (parseDebug) + System.out.println(" To addresses:"); + to = parseAddressList(r); + if (parseDebug) + System.out.println(" Cc addresses:"); + cc = parseAddressList(r); + if (parseDebug) + System.out.println(" Bcc addresses:"); + bcc = parseAddressList(r); + inReplyTo = r.readString(); + if (parseDebug) + System.out.println(" In-Reply-To: " + inReplyTo); + messageId = r.readString(); + if (parseDebug) + System.out.println(" Message-ID: " + messageId); + + if (!r.isNextNonSpace(')')) + throw new ParsingException("ENVELOPE parse error"); + } + + private InternetAddress[] parseAddressList(Response r) + throws ParsingException { + r.skipSpaces(); // skip leading spaces + + byte b = r.readByte(); + if (b == '(') { + /* + * Some broken servers (e.g., Yahoo Mail) return an empty + * list instead of NIL. Handle that here even though it + * doesn't conform to the IMAP spec. + */ + if (r.isNextNonSpace(')')) + return null; + + List v = new ArrayList<>(); + + do { + IMAPAddress a = new IMAPAddress(r); + if (parseDebug) + System.out.println(" Address: " + a); + // if we see an end-of-group address at the top, ignore it + if (!a.isEndOfGroup()) + v.add(a); + } while (!r.isNextNonSpace(')')); + + return v.toArray(new InternetAddress[v.size()]); + } else if (b == 'N' || b == 'n') { // NIL + r.skip(2); // skip 'NIL' + return null; + } else + throw new ParsingException("ADDRESS parse error"); + } +} + +class IMAPAddress extends InternetAddress { + private boolean group = false; + private InternetAddress[] grouplist; + private String groupname; + + private static final long serialVersionUID = -3835822029483122232L; + + IMAPAddress(Response r) throws ParsingException { + r.skipSpaces(); // skip leading spaces + + if (r.readByte() != '(') + throw new ParsingException("ADDRESS parse error"); + + encodedPersonal = r.readString(); + + r.readString(); // throw away address_list + String mb = r.readString(); + String host = r.readString(); + // skip bogus spaces inserted by Yahoo IMAP server if + // "undisclosed-recipients" is a recipient + r.skipSpaces(); + if (!r.isNextNonSpace(')')) // skip past terminating ')' + throw new ParsingException("ADDRESS parse error"); + + if (host == null) { + // it's a group list, start or end + group = true; + groupname = mb; + if (groupname == null) // end of group list + return; + // Accumulate a group list. The members of the group + // are accumulated in a List and the corresponding string + // representation of the group is accumulated in a StringBuilder. + StringBuilder sb = new StringBuilder(); + sb.append(groupname).append(':'); + List v = new ArrayList<>(); + while (r.peekByte() != ')') { + IMAPAddress a = new IMAPAddress(r); + if (a.isEndOfGroup()) // reached end of group + break; + if (v.size() != 0) // if not first element, need a comma + sb.append(','); + sb.append(a.toString()); + v.add(a); + } + sb.append(';'); + address = sb.toString(); + grouplist = v.toArray(new IMAPAddress[v.size()]); + } else { + if (mb == null || mb.length() == 0) + address = host; + else if (host.length() == 0) + address = mb; + else + address = mb + "@" + host; + } + + } + + boolean isEndOfGroup() { + return group && groupname == null; + } + + @Override + public boolean isGroup() { + return group; + } + + @Override + public InternetAddress[] getGroup(boolean strict) throws AddressException { + if (grouplist == null) + return null; + return grouplist.clone(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/FLAGS.java b/app/src/main/java/com/sun/mail/imap/protocol/FLAGS.java new file mode 100644 index 0000000000..5621aa81c3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/FLAGS.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import javax.mail.Flags; +import com.sun.mail.iap.*; + +/** + * This class + * + * @author John Mani + */ + +public class FLAGS extends Flags implements Item { + + // IMAP item name + static final char[] name = {'F','L','A','G','S'}; + public int msgno; + + private static final long serialVersionUID = 439049847053756670L; + + /** + * Constructor. + * + * @param r the IMAPResponse + * @exception ParsingException for parsing failures + */ + public FLAGS(IMAPResponse r) throws ParsingException { + msgno = r.getNumber(); + + r.skipSpaces(); + String[] flags = r.readSimpleList(); + if (flags != null) { // if not empty flaglist + for (int i = 0; i < flags.length; i++) { + String s = flags[i]; + if (s.length() >= 2 && s.charAt(0) == '\\') { + switch (Character.toUpperCase(s.charAt(1))) { + case 'S': // \Seen + add(Flags.Flag.SEEN); + break; + case 'R': // \Recent + add(Flags.Flag.RECENT); + break; + case 'D': + if (s.length() >= 3) { + char c = s.charAt(2); + if (c == 'e' || c == 'E') // \Deleted + add(Flags.Flag.DELETED); + else if (c == 'r' || c == 'R') // \Draft + add(Flags.Flag.DRAFT); + } else + add(s); // unknown, treat it as a user flag + break; + case 'A': // \Answered + add(Flags.Flag.ANSWERED); + break; + case 'F': // \Flagged + add(Flags.Flag.FLAGGED); + break; + case '*': // \* + add(Flags.Flag.USER); + break; + default: + add(s); // unknown, treat it as a user flag + break; + } + } else + add(s); + } + } + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/FetchItem.java b/app/src/main/java/com/sun/mail/imap/protocol/FetchItem.java new file mode 100644 index 0000000000..1eaae6c1c7 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/FetchItem.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.lang.reflect.*; + +import javax.mail.FetchProfile; +import com.sun.mail.iap.ParsingException; + +/** + * Metadata describing a FETCH item. + * Note that the "name" field MUST be in uppercase.

+ * + * @author Bill Shannon + * @since JavaMail 1.4.6 + */ + +public abstract class FetchItem { + private String name; + private FetchProfile.Item fetchProfileItem; + + public FetchItem(String name, FetchProfile.Item fetchProfileItem) { + this.name = name; + this.fetchProfileItem = fetchProfileItem; + } + + public String getName() { + return name; + } + + public FetchProfile.Item getFetchProfileItem() { + return fetchProfileItem; + } + + /** + * Parse the item into some kind of object appropriate for the item. + * Note that the item name will have been parsed and skipped already. + * + * @param r the response + * @return the fetch item + * @exception ParsingException for parsing failures + */ + public abstract Object parseItem(FetchResponse r) throws ParsingException; +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/FetchResponse.java b/app/src/main/java/com/sun/mail/imap/protocol/FetchResponse.java new file mode 100644 index 0000000000..6477ba5b2d --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/FetchResponse.java @@ -0,0 +1,320 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.util.*; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.iap.*; + +/** + * This class represents a FETCH response obtained from the input stream + * of an IMAP server. + * + * @author John Mani + * @author Bill Shannon + */ + +public class FetchResponse extends IMAPResponse { + /* + * Regular Items are saved in the items array. + * Extension items (items handled by subclasses + * that extend the IMAP provider) are saved in the + * extensionItems map, indexed by the FETCH item name. + * The map is only created when needed. + * + * XXX - Should consider unifying the handling of + * regular items and extension items. + */ + private Item[] items; + private Map extensionItems; + private final FetchItem[] fitems; + + public FetchResponse(Protocol p) + throws IOException, ProtocolException { + super(p); + fitems = null; + parse(); + } + + public FetchResponse(IMAPResponse r) + throws IOException, ProtocolException { + this(r, null); + } + + /** + * Construct a FetchResponse that handles the additional FetchItems. + * + * @param r the IMAPResponse + * @param fitems the fetch items + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + * @since JavaMail 1.4.6 + */ + public FetchResponse(IMAPResponse r, FetchItem[] fitems) + throws IOException, ProtocolException { + super(r); + this.fitems = fitems; + parse(); + } + + public int getItemCount() { + return items.length; + } + + public Item getItem(int index) { + return items[index]; + } + + public T getItem(Class c) { + for (int i = 0; i < items.length; i++) { + if (c.isInstance(items[i])) + return c.cast(items[i]); + } + + return null; + } + + /** + * Return the first fetch response item of the given class + * for the given message number. + * + * @param r the responses + * @param msgno the message number + * @param c the class + * @param the type of fetch item + * @return the fetch item + */ + public static T getItem(Response[] r, int msgno, + Class c) { + if (r == null) + return null; + + for (int i = 0; i < r.length; i++) { + + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != msgno) + continue; + + FetchResponse f = (FetchResponse)r[i]; + for (int j = 0; j < f.items.length; j++) { + if (c.isInstance(f.items[j])) + return c.cast(f.items[j]); + } + } + + return null; + } + + /** + * Return all fetch response items of the given class + * for the given message number. + * + * @param r the responses + * @param msgno the message number + * @param c the class + * @param the type of fetch items + * @return the list of fetch items + * @since JavaMail 1.5.2 + */ + public static List getItems(Response[] r, int msgno, + Class c) { + List items = new ArrayList<>(); + + if (r == null) + return items; + + for (int i = 0; i < r.length; i++) { + + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != msgno) + continue; + + FetchResponse f = (FetchResponse)r[i]; + for (int j = 0; j < f.items.length; j++) { + if (c.isInstance(f.items[j])) + items.add(c.cast(f.items[j])); + } + } + + return items; + } + + /** + * Return a map of the extension items found in this fetch response. + * The map is indexed by extension item name. Callers should not + * modify the map. + * + * @return Map of extension items, or null if none + * @since JavaMail 1.4.6 + */ + public Map getExtensionItems() { + return extensionItems; + } + + private final static char[] HEADER = {'.','H','E','A','D','E','R'}; + private final static char[] TEXT = {'.','T','E','X','T'}; + + private void parse() throws ParsingException { + if (!isNextNonSpace('(')) + throw new ParsingException( + "error in FETCH parsing, missing '(' at index " + index); + + List v = new ArrayList<>(); + Item i = null; + skipSpaces(); + do { + + if (index >= size) + throw new ParsingException( + "error in FETCH parsing, ran off end of buffer, size " + size); + + i = parseItem(); + if (i != null) + v.add(i); + else if (!parseExtensionItem()) + throw new ParsingException( + "error in FETCH parsing, unrecognized item at index " + + index + ", starts with \"" + next20() + "\""); + } while (!isNextNonSpace(')')); + + items = v.toArray(new Item[v.size()]); + } + + /** + * Return the next 20 characters in the buffer, for exception messages. + */ + private String next20() { + if (index + 20 > size) + return ASCIIUtility.toString(buffer, index, size); + else + return ASCIIUtility.toString(buffer, index, index + 20) + "..."; + } + + /** + * Parse the item at the current position in the buffer, + * skipping over the item if successful. Otherwise, return null + * and leave the buffer position unmodified. + */ + @SuppressWarnings("empty") + private Item parseItem() throws ParsingException { + switch (buffer[index]) { + case 'E': case 'e': + if (match(ENVELOPE.name)) + return new ENVELOPE(this); + break; + case 'F': case 'f': + if (match(FLAGS.name)) + return new FLAGS((IMAPResponse)this); + break; + case 'I': case 'i': + if (match(INTERNALDATE.name)) + return new INTERNALDATE(this); + break; + case 'B': case 'b': + if (match(BODYSTRUCTURE.name)) + return new BODYSTRUCTURE(this); + else if (match(BODY.name)) { + if (buffer[index] == '[') + return new BODY(this); + else + return new BODYSTRUCTURE(this); + } + break; + case 'R': case 'r': + if (match(RFC822SIZE.name)) + return new RFC822SIZE(this); + else if (match(RFC822DATA.name)) { + boolean isHeader = false; + if (match(HEADER)) + isHeader = true; // skip ".HEADER" + else if (match(TEXT)) + isHeader = false; // skip ".TEXT" + return new RFC822DATA(this, isHeader); + } + break; + case 'U': case 'u': + if (match(UID.name)) + return new UID(this); + break; + case 'M': case 'm': + if (match(MODSEQ.name)) + return new MODSEQ(this); + break; + default: + break; + } + return null; + } + + /** + * If this item is a known extension item, parse it. + */ + private boolean parseExtensionItem() throws ParsingException { + if (fitems == null) + return false; + for (int i = 0; i < fitems.length; i++) { + if (match(fitems[i].getName())) { + if (extensionItems == null) + extensionItems = new HashMap<>(); + extensionItems.put(fitems[i].getName(), + fitems[i].parseItem(this)); + return true; + } + } + return false; + } + + /** + * Does the current buffer match the given item name? + * itemName is the name of the IMAP item to compare against. + * NOTE that itemName *must* be all uppercase. + * If the match is successful, the buffer pointer (index) + * is incremented past the matched item. + */ + private boolean match(char[] itemName) { + int len = itemName.length; + for (int i = 0, j = index; i < len;) + // IMAP tokens are case-insensitive. We store itemNames in + // uppercase, so convert operand to uppercase before comparing. + if (Character.toUpperCase((char)buffer[j++]) != itemName[i++]) + return false; + index += len; + return true; + } + + /** + * Does the current buffer match the given item name? + * itemName is the name of the IMAP item to compare against. + * NOTE that itemName *must* be all uppercase. + * If the match is successful, the buffer pointer (index) + * is incremented past the matched item. + */ + private boolean match(String itemName) { + int len = itemName.length(); + for (int i = 0, j = index; i < len;) + // IMAP tokens are case-insensitive. We store itemNames in + // uppercase, so convert operand to uppercase before comparing. + if (Character.toUpperCase((char)buffer[j++]) != + itemName.charAt(i++)) + return false; + index += len; + return true; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/ID.java b/app/src/main/java/com/sun/mail/imap/protocol/ID.java new file mode 100644 index 0000000000..66cb865fcc --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/ID.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.*; +import com.sun.mail.iap.*; + +/** + * This class represents the response to the ID command.

+ * + * See RFC 2971. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class ID { + + private Map serverParams = null; + + /** + * Parse the server parameter list out of the response. + * + * @param r the response + * @exception ProtocolException for protocol failures + */ + public ID(Response r) throws ProtocolException { + // id_response ::= "ID" SPACE id_params_list + // id_params_list ::= "(" #(string SPACE nstring) ")" / nil + // ;; list of field value pairs + + r.skipSpaces(); + int c = r.peekByte(); + if (c == 'N' || c == 'n') // assume NIL + return; + + if (c != '(') + throw new ProtocolException("Missing '(' at start of ID"); + + serverParams = new HashMap<>(); + + String[] v = r.readStringList(); + if (v != null) { + for (int i = 0; i < v.length; i += 2) { + String name = v[i]; + if (name == null) + throw new ProtocolException("ID field name null"); + if (i + 1 >= v.length) + throw new ProtocolException("ID field without value: " + + name); + String value = v[i + 1]; + serverParams.put(name, value); + } + } + serverParams = Collections.unmodifiableMap(serverParams); + } + + /** + * Return the parsed server params. + */ + Map getServerParams() { + return serverParams; + } + + /** + * Convert the client parameters into an argument list for the ID command. + */ + static Argument getArgumentList(Map clientParams) { + Argument arg = new Argument(); + if (clientParams == null) { + arg.writeAtom("NIL"); + return arg; + } + Argument list = new Argument(); + // add params to list + for (Map.Entry e : clientParams.entrySet()) { + list.writeNString(e.getKey()); // assume these are ASCII only + list.writeNString(e.getValue()); + } + arg.writeArgument(list); + return arg; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java b/app/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java new file mode 100644 index 0000000000..a8a88b6c01 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/IMAPProtocol.java @@ -0,0 +1,3294 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.util.*; +import java.text.*; +import java.lang.reflect.*; +import java.util.logging.Level; +import java.nio.charset.StandardCharsets; + +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.search.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.iap.*; +import com.sun.mail.auth.Ntlm; + +import com.sun.mail.imap.ACL; +import com.sun.mail.imap.Rights; +import com.sun.mail.imap.AppendUID; +import com.sun.mail.imap.CopyUID; +import com.sun.mail.imap.SortTerm; +import com.sun.mail.imap.ResyncData; +import com.sun.mail.imap.Utility; + +/** + * This class extends the iap.Protocol object and implements IMAP + * semantics. In general, there is a method corresponding to each + * IMAP protocol command. The typical implementation issues the + * appropriate protocol command, collects all responses, processes + * those responses that are specific to this command and then + * dispatches the rest (the unsolicited ones) to the dispatcher + * using the notifyResponseHandlers(r). + * + * @author John Mani + * @author Bill Shannon + */ + +public class IMAPProtocol extends Protocol { + + private boolean connected = false; // did constructor succeed? + private boolean rev1 = false; // REV1 server ? + private boolean referralException; // throw exception for IMAP REFERRAL? + private boolean noauthdebug = true; // hide auth info in debug output + private boolean authenticated; // authenticated? + // WARNING: authenticated may be set to true in superclass + // constructor, don't initialize it here. + + private Map capabilities; + // WARNING: capabilities may be initialized as a result of superclass + // constructor, don't initialize it here. + private List authmechs; + // WARNING: authmechs may be initialized as a result of superclass + // constructor, don't initialize it here. + private boolean utf8; // UTF-8 support enabled? + + protected SearchSequence searchSequence; + protected String[] searchCharsets; // array of search charsets + + protected Set enabled; // enabled capabilities - RFC 5161 + + private String name; + private SaslAuthenticator saslAuthenticator; // if SASL is being used + private String proxyAuthUser; // user name used with PROXYAUTH + + private ByteArray ba; // a buffer for fetchBody + + private static final byte[] CRLF = { (byte)'\r', (byte)'\n'}; + + private static final FetchItem[] fetchItems = { }; + + /** + * Constructor. + * Opens a connection to the given host at given port. + * + * @param name the protocol name + * @param host host to connect to + * @param port port number to connect to + * @param props Properties object used by this protocol + * @param isSSL true if SSL should be used + * @param logger the MailLogger to use for debug output + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public IMAPProtocol(String name, String host, int port, + Properties props, boolean isSSL, MailLogger logger) + throws IOException, ProtocolException { + super(host, port, props, "mail." + name, isSSL, logger); + + try { + this.name = name; + noauthdebug = + !PropUtil.getBooleanProperty(props, "mail.debug.auth", false); + + // in case it was not initialized in processGreeting + referralException = PropUtil.getBooleanProperty(props, + prefix + ".referralexception", false); + + if (capabilities == null) + capability(); + + if (hasCapability("IMAP4rev1")) + rev1 = true; + + searchCharsets = new String[2]; // 2, for now. + searchCharsets[0] = "UTF-8"; + searchCharsets[1] = MimeUtility.mimeCharset( + MimeUtility.getDefaultJavaCharset() + ); + + connected = true; // must be last statement in constructor + } finally { + /* + * If we get here because an exception was thrown, we need + * to disconnect to avoid leaving a connected socket that + * no one will be able to use because this object was never + * completely constructed. + */ + if (!connected) + disconnect(); + } + } + + /** + * Constructor for debugging. + * + * @param in the InputStream from which to read + * @param out the PrintStream to which to write + * @param props Properties object used by this protocol + * @param debug true to enable debugging output + * @exception IOException for I/O errors + */ + public IMAPProtocol(InputStream in, PrintStream out, + Properties props, boolean debug) + throws IOException { + super(in, out, props, debug); + + this.name = "imap"; + noauthdebug = + !PropUtil.getBooleanProperty(props, "mail.debug.auth", false); + + if (capabilities == null) + capabilities = new HashMap<>(); + + searchCharsets = new String[2]; // 2, for now. + searchCharsets[0] = "UTF-8"; + searchCharsets[1] = MimeUtility.mimeCharset( + MimeUtility.getDefaultJavaCharset() + ); + + connected = true; // must be last statement in constructor + } + + /** + * Return an array of FetchItem objects describing the + * FETCH items supported by this protocol. Subclasses may + * override this method to combine their FetchItems with + * the FetchItems returned by the superclass. + * + * @return an array of FetchItem objects + * @since JavaMail 1.4.6 + */ + public FetchItem[] getFetchItems() { + return fetchItems; + } + + /** + * CAPABILITY command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.1.1" + */ + public void capability() throws ProtocolException { + // Check CAPABILITY + Response[] r = command("CAPABILITY", null); + Response response = r[r.length-1]; + + if (response.isOK()) + handleCapabilityResponse(r); + handleResult(response); + } + + /** + * Handle any untagged CAPABILITY response in the Response array. + * + * @param r the responses + */ + public void handleCapabilityResponse(Response[] r) { + boolean first = true; + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + + // Handle *all* untagged CAPABILITY responses. + // Though the spec seemingly states that only + // one CAPABILITY response string is allowed (6.1.1), + // some server vendors claim otherwise. + if (ir.keyEquals("CAPABILITY")) { + if (first) { + // clear out current when first response seen + capabilities = new HashMap<>(10); + authmechs = new ArrayList<>(5); + first = false; + } + parseCapabilities(ir); + } + } + } + + /** + * If the response contains a CAPABILITY response code, extract + * it and save the capabilities. + * + * @param r the response + */ + protected void setCapabilities(Response r) { + byte b; + while ((b = r.readByte()) > 0 && b != (byte)'[') + ; + if (b == 0) + return; + String s; + s = r.readAtom(); + if (!s.equalsIgnoreCase("CAPABILITY")) + return; + capabilities = new HashMap<>(10); + authmechs = new ArrayList<>(5); + parseCapabilities(r); + } + + /** + * Parse the capabilities from a CAPABILITY response or from + * a CAPABILITY response code attached to (e.g.) an OK response. + * + * @param r the CAPABILITY response + */ + protected void parseCapabilities(Response r) { + String s; + while ((s = r.readAtom()) != null) { + if (s.length() == 0) { + if (r.peekByte() == (byte)']') + break; + /* + * Probably found something here that's not an atom. + * Rather than loop forever or fail completely, we'll + * try to skip this bogus capability. This is known + * to happen with: + * Netscape Messaging Server 4.03 (built Apr 27 1999) + * that returns: + * * CAPABILITY * CAPABILITY IMAP4 IMAP4rev1 ... + * The "*" in the middle of the capability list causes + * us to loop forever here. + */ + r.skipToken(); + } else { + capabilities.put(s.toUpperCase(Locale.ENGLISH), s); + if (s.regionMatches(true, 0, "AUTH=", 0, 5)) { + authmechs.add(s.substring(5)); + if (logger.isLoggable(Level.FINE)) + logger.fine("AUTH: " + s.substring(5)); + } + } + } + } + + /** + * Check the greeting when first connecting; look for PREAUTH response. + * + * @param r the greeting response + * @exception ProtocolException for protocol failures + */ + @Override + protected void processGreeting(Response r) throws ProtocolException { + if (r.isBYE()) { + checkReferral(r); // may throw exception + throw new ConnectionException(this, r); + } + if (r.isOK()) { // check if it's OK + // XXX - is a REFERRAL response code really allowed here? + // XXX - referralException hasn't been initialized in c'tor yet + referralException = PropUtil.getBooleanProperty(props, + prefix + ".referralexception", false); + if (referralException) + checkReferral(r); + setCapabilities(r); + return; + } + // only other choice is PREAUTH + assert r instanceof IMAPResponse; + IMAPResponse ir = (IMAPResponse)r; + if (ir.keyEquals("PREAUTH")) { + authenticated = true; + setCapabilities(r); + } else { + disconnect(); + throw new ConnectionException(this, r); + } + } + + /** + * Check for an IMAP login REFERRAL response code. + * + * @exception IMAPReferralException if REFERRAL response code found + * @see "RFC 2221" + */ + private void checkReferral(Response r) throws IMAPReferralException { + String s = r.getRest(); // get the text after the response + if (s.startsWith("[")) { // a response code + int i = s.indexOf(' '); + if (i > 0 && s.substring(1, i).equalsIgnoreCase("REFERRAL")) { + String url, msg; + int j = s.indexOf(']'); + if (j > 0) { // should always be true; + url = s.substring(i + 1, j); + msg = s.substring(j + 1).trim(); + } else { + url = s.substring(i + 1); + msg = ""; + } + if (r.isBYE()) + disconnect(); + throw new IMAPReferralException(msg, url); + } + } + } + + /** + * Returns true if the connection has been authenticated, + * either due to a successful login, or due to a PREAUTH greeting response. + * + * @return true if the connection has been authenticated + */ + public boolean isAuthenticated() { + return authenticated; + } + + /** + * Returns true if this is an IMAP4rev1 server + * + * @return true if this is an IMAP4rev1 server + */ + public boolean isREV1() { + return rev1; + } + + /** + * Returns whether this Protocol supports non-synchronizing literals. + * + * @return true if non-synchronizing literals are supported + */ + @Override + protected boolean supportsNonSyncLiterals() { + return hasCapability("LITERAL+"); + } + + /** + * Read a response from the server. + * + * @return the response + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + @Override + public Response readResponse() throws IOException, ProtocolException { + // assert Thread.holdsLock(this); + // can't assert because it's called from constructor + IMAPResponse r = new IMAPResponse(this); + if (r.keyEquals("FETCH")) + r = new FetchResponse(r, getFetchItems()); + return r; + } + + /** + * Check whether the given capability is supported by + * this server. Returns true if so, otherwise + * returns false. + * + * @param c the capability name + * @return true if the server has the capability + */ + public boolean hasCapability(String c) { + if (c.endsWith("*")) { + c = c.substring(0, c.length() - 1).toUpperCase(Locale.ENGLISH); + Iterator it = capabilities.keySet().iterator(); + while (it.hasNext()) { + if (it.next().startsWith(c)) + return true; + } + return false; + } + return capabilities.containsKey(c.toUpperCase(Locale.ENGLISH)); + } + + /** + * Return the map of capabilities returned by the server. + * + * @return the Map of capabilities + * @since JavaMail 1.4.1 + */ + public Map getCapabilities() { + return capabilities; + } + + /** + * Does the server support UTF-8? + * + * @since JavaMail 1.6.0 + */ + public boolean supportsUtf8() { + return utf8; + } + + /** + * Close socket connection. + * + * This method just makes the Protocol.disconnect() method + * public. + */ + @Override + public void disconnect() { + super.disconnect(); + authenticated = false; // just in case + } + + /** + * The NOOP command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.1.2" + */ + public void noop() throws ProtocolException { + logger.fine("IMAPProtocol noop"); + simpleCommand("NOOP", null); + } + + /** + * LOGOUT Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.1.3" + */ + public void logout() throws ProtocolException { + try { + Response[] r = command("LOGOUT", null); + + authenticated = false; + // dispatch any unsolicited responses. + // NOTE that the BYE response is dispatched here as well + notifyResponseHandlers(r); + } finally { + disconnect(); + } + } + + /** + * LOGIN Command. + * + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC2060, section 6.2.2" + */ + public void login(String u, String p) throws ProtocolException { + Argument args = new Argument(); + args.writeString(u); + args.writeString(p); + + Response[] r = null; + try { + if (noauthdebug && isTracing()) { + logger.fine("LOGIN command trace suppressed"); + suspendTracing(); + } + r = command("LOGIN", args); + } finally { + resumeTracing(); + } + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(r); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + if (noauthdebug && isTracing()) + logger.fine("LOGIN command result: " + r[r.length-1]); + handleLoginResult(r[r.length-1]); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r[r.length-1]); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * The AUTHENTICATE command with AUTH=LOGIN authenticate scheme + * + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC2060, section 6.2.1" + */ + public synchronized void authlogin(String u, String p) + throws ProtocolException { + List v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE LOGIN command trace suppressed"); + suspendTracing(); + } + + try { + tag = writeCommand("AUTHENTICATE LOGIN", null); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + + /* Wrap a BASE64Encoder around a ByteArrayOutputstream + * to craft b64 encoded username and password strings + * + * Note that the encoded bytes should be sent "as-is" to the + * server, *not* as literals or quoted-strings. + * + * Also note that unlike the B64 definition in MIME, CRLFs + * should *not* be inserted during the encoding process. So, I + * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine, + * which should be sufficiently large ! + * + * Finally, format the line in a buffer so it can be sent as + * a single packet, to avoid triggering a bug in SUN's SIMS 2.0 + * server caused by patch 105346. + */ + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); + boolean first = true; + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + String s; + if (first) { // Send encoded username + s = u; + first = false; + } else // Send encoded password + s = p; + + // obtain b64 encoded bytes + b64os.write(s.getBytes(StandardCharsets.UTF_8)); + b64os.flush(); // complete the encoding + + bos.write(CRLF); // CRLF termination + os.write(bos.toByteArray()); // write out line + os.flush(); // flush the stream + bos.reset(); // reset buffer + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE LOGIN command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + + /** + * The AUTHENTICATE command with AUTH=PLAIN authentication scheme. + * This is based heavly on the {@link #authlogin} method. + * + * @param authzid the authorization id + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC3501, section 6.2.2" + * @see "RFC2595, section 6" + * @since JavaMail 1.3.2 + */ + public synchronized void authplain(String authzid, String u, String p) + throws ProtocolException { + List v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE PLAIN command trace suppressed"); + suspendTracing(); + } + + try { + tag = writeCommand("AUTHENTICATE PLAIN", null); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + + /* Wrap a BASE64Encoder around a ByteArrayOutputstream + * to craft b64 encoded username and password strings + * + * Note that the encoded bytes should be sent "as-is" to the + * server, *not* as literals or quoted-strings. + * + * Also note that unlike the B64 definition in MIME, CRLFs + * should *not* be inserted during the encoding process. So, I + * use Integer.MAX_VALUE (0x7fffffff (> 1G)) as the bytesPerLine, + * which should be sufficiently large ! + * + * Finally, format the line in a buffer so it can be sent as + * a single packet, to avoid triggering a bug in SUN's SIMS 2.0 + * server caused by patch 105346. + */ + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + final String nullByte = "\0"; + String s = (authzid == null ? "" : authzid) + + nullByte + u + nullByte + p; + + // obtain b64 encoded bytes + b64os.write(s.getBytes(StandardCharsets.UTF_8)); + b64os.flush(); // complete the encoding + + bos.write(CRLF); // CRLF termination + os.write(bos.toByteArray()); // write out line + os.flush(); // flush the stream + bos.reset(); // reset buffer + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE PLAIN command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * The AUTHENTICATE command with AUTH=NTLM authentication scheme. + * This is based heavly on the {@link #authlogin} method. + * + * @param authzid the authorization id + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC3501, section 6.2.2" + * @see "RFC2595, section 6" + * @since JavaMail 1.4.3 + */ + public synchronized void authntlm(String authzid, String u, String p) + throws ProtocolException { + List v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + String type1Msg = null; + int flags = PropUtil.getIntProperty(props, + "mail." + name + ".auth.ntlm.flags", 0); + boolean v2 = PropUtil.getBooleanProperty(props, + "mail." + name + ".auth.ntlm.v2", true); + String domain = props.getProperty( + "mail." + name + ".auth.ntlm.domain", ""); + Ntlm ntlm = new Ntlm(domain, getLocalHost(), u, p, logger); + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE NTLM command trace suppressed"); + suspendTracing(); + } + + try { + tag = writeCommand("AUTHENTICATE NTLM", null); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + boolean first = true; + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + String s; + if (first) { + s = ntlm.generateType1Msg(flags, v2); + first = false; + } else { + s = ntlm.generateType3Msg(r.getRest()); + } + + os.write(s.getBytes(StandardCharsets.UTF_8)); + os.write(CRLF); // CRLF termination + os.flush(); // flush the stream + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE NTLM command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * The AUTHENTICATE command with AUTH=XOAUTH2 authentication scheme. + * This is based heavly on the {@link #authlogin} method. + * + * @param u the username + * @param p the password + * @throws ProtocolException as thrown by {@link Protocol#handleResult}. + * @see "RFC3501, section 6.2.2" + * @see "RFC2595, section 6" + * @since JavaMail 1.5.5 + */ + public synchronized void authoauth2(String u, String p) + throws ProtocolException { + List v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("AUTHENTICATE XOAUTH2 command trace suppressed"); + suspendTracing(); + } + + try { + Argument args = new Argument(); + args.writeAtom("XOAUTH2"); + if (hasCapability("SASL-IR")) { + String resp = "user=" + u + "\001auth=Bearer " + p + "\001\001"; + byte[] ba = BASE64EncoderStream.encode( + resp.getBytes(StandardCharsets.UTF_8)); + String irs = ASCIIUtility.toString(ba, 0, ba.length); + args.writeAtom(irs); + } + tag = writeCommand("AUTHENTICATE", args); + } catch (Exception ex) { + // Convert this into a BYE response + r = Response.byeResponse(ex); + done = true; + } + + OutputStream os = getOutputStream(); // stream to IMAP server + + while (!done) { // loop till we are done + try { + r = readResponse(); + if (r.isContinuation()) { + // Server challenge .. + String resp = "user=" + u + "\001auth=Bearer " + + p + "\001\001"; + byte[] b = BASE64EncoderStream.encode( + resp.getBytes(StandardCharsets.UTF_8)); + os.write(b); // write out response + os.write(CRLF); // CRLF termination + os.flush(); // flush the stream + } else if (r.isTagged() && r.getTag().equals(tag)) + // Ah, our tagged response + done = true; + else if (r.isBYE()) // outta here + done = true; + // hmm .. unsolicited response here ?! + } catch (Exception ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + } + v.add(r); + } + + } finally { + resumeTracing(); + } + + Response[] responses = v.toArray(new Response[v.size()]); + + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(responses); + + /* + * Dispatch untagged responses. + * NOTE: in our current upper level IMAP classes, we add the + * responseHandler to the Protocol object only *after* the + * connection has been authenticated. So, for now, the below + * code really ends up being just a no-op. + */ + notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + if (noauthdebug && isTracing()) + logger.fine("AUTHENTICATE XOAUTH2 command result: " + r); + handleLoginResult(r); + // If the response includes a CAPABILITY response code, process it + setCapabilities(r); + // if we get this far without an exception, we're authenticated + authenticated = true; + } + + /** + * SASL-based login. + * + * @param allowed the SASL mechanisms we're allowed to use + * @param realm the SASL realm + * @param authzid the authorization id + * @param u the username + * @param p the password + * @exception ProtocolException for protocol failures + */ + public void sasllogin(String[] allowed, String realm, String authzid, + String u, String p) throws ProtocolException { + boolean useCanonicalHostName = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.usecanonicalhostname", false); + String serviceHost; + if (useCanonicalHostName) + serviceHost = getInetAddress().getCanonicalHostName(); + else + serviceHost = host; + if (saslAuthenticator == null) { + try { + Class sac = Class.forName( + "com.sun.mail.imap.protocol.IMAPSaslAuthenticator"); + Constructor c = sac.getConstructor(new Class[] { + IMAPProtocol.class, + String.class, + Properties.class, + MailLogger.class, + String.class + }); + saslAuthenticator = (SaslAuthenticator)c.newInstance( + new Object[] { + this, + name, + props, + logger, + serviceHost + }); + } catch (Exception ex) { + logger.log(Level.FINE, "Can't load SASL authenticator", ex); + // probably because we're running on a system without SASL + return; // not authenticated, try without SASL + } + } + + // were any allowed mechanisms specified? + List v; + if (allowed != null && allowed.length > 0) { + // remove anything not supported by the server + v = new ArrayList<>(allowed.length); + for (int i = 0; i < allowed.length; i++) + if (authmechs.contains(allowed[i])) // XXX - case must match + v.add(allowed[i]); + } else { + // everything is allowed + v = authmechs; + } + String[] mechs = v.toArray(new String[v.size()]); + + try { + + if (noauthdebug && isTracing()) { + logger.fine("SASL authentication command trace suppressed"); + suspendTracing(); + } + + if (saslAuthenticator.authenticate(mechs, realm, authzid, u, p)) { + if (noauthdebug && isTracing()) + logger.fine("SASL authentication succeeded"); + authenticated = true; + } else { + if (noauthdebug && isTracing()) + logger.fine("SASL authentication failed"); + } + } finally { + resumeTracing(); + } + } + + // XXX - for IMAPSaslAuthenticator access to protected method + OutputStream getIMAPOutputStream() { + return getOutputStream(); + } + + /** + * Handle the result response for a LOGIN or AUTHENTICATE command. + * Look for IMAP login REFERRAL. + * + * @param r the response + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.5 + */ + protected void handleLoginResult(Response r) throws ProtocolException { + if (hasCapability("LOGIN-REFERRALS") && + (!r.isOK() || referralException)) + checkReferral(r); + handleResult(r); + } + + /** + * PROXYAUTH Command. + * + * @param u the PROXYAUTH user name + * @exception ProtocolException for protocol failures + * @see "Netscape/iPlanet/SunONE Messaging Server extension" + */ + public void proxyauth(String u) throws ProtocolException { + Argument args = new Argument(); + args.writeString(u); + + simpleCommand("PROXYAUTH", args); + proxyAuthUser = u; + } + + /** + * Get the user name used with the PROXYAUTH command. + * Returns null if PROXYAUTH was not used. + * + * @return the PROXYAUTH user name + * @since JavaMail 1.5.1 + */ + public String getProxyAuthUser() { + return proxyAuthUser; + } + + /** + * UNAUTHENTICATE Command. + * + * @exception ProtocolException for protocol failures + * @see "Netscape/iPlanet/SunONE Messaging Server extension" + * @since JavaMail 1.5.1 + */ + public void unauthenticate() throws ProtocolException { + if (!hasCapability("X-UNAUTHENTICATE")) + throw new BadCommandException("UNAUTHENTICATE not supported"); + simpleCommand("UNAUTHENTICATE", null); + authenticated = false; + } + + /** + * ID Command, for Yahoo! Mail IMAP server. + * + * @param guid the GUID + * @exception ProtocolException for protocol failures + * @deprecated As of JavaMail 1.5.1, replaced by + * {@link #id(Map) id(Map<String,String>)} + * @since JavaMail 1.4.4 + */ + @Deprecated + public void id(String guid) throws ProtocolException { + // support this for now, but remove it soon + Map gmap = new HashMap<>(); + gmap.put("GUID", guid); + id(gmap); + } + + /** + * STARTTLS Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC3501, section 6.2.1" + */ + public void startTLS() throws ProtocolException { + try { + super.startTLS("STARTTLS"); + } catch (ProtocolException pex) { + logger.log(Level.FINE, "STARTTLS ProtocolException", pex); + // ProtocolException just means the command wasn't recognized, + // or failed. This should never happen if we check the + // CAPABILITY first. + throw pex; + } catch (Exception ex) { + logger.log(Level.FINE, "STARTTLS Exception", ex); + // any other exception means we have to shut down the connection + // generate an artificial BYE response and disconnect + Response[] r = { Response.byeResponse(ex) }; + notifyResponseHandlers(r); + disconnect(); + throw new ProtocolException("STARTTLS failure", ex); + } + } + + /** + * COMPRESS Command. Only supports DEFLATE. + * + * @exception ProtocolException for protocol failures + * @see "RFC 4978" + */ + public void compress() throws ProtocolException { + try { + super.startCompression("COMPRESS DEFLATE"); + } catch (ProtocolException pex) { + logger.log(Level.FINE, "COMPRESS ProtocolException", pex); + // ProtocolException just means the command wasn't recognized, + // or failed. This should never happen if we check the + // CAPABILITY first. + throw pex; + } catch (Exception ex) { + logger.log(Level.FINE, "COMPRESS Exception", ex); + // any other exception means we have to shut down the connection + // generate an artificial BYE response and disconnect + Response[] r = { Response.byeResponse(ex) }; + notifyResponseHandlers(r); + disconnect(); + throw new ProtocolException("COMPRESS failure", ex); + } + } + + /** + * Encode a mailbox name appropriately depending on whether or not + * the server supports UTF-8, and add the encoded name to the + * Argument. + * + * @param args the arguments + * @param name the name to encode + * @since JavaMail 1.6.0 + */ + protected void writeMailboxName(Argument args, String name) { + if (utf8) + args.writeString(name, StandardCharsets.UTF_8); + else + // encode the mbox as per RFC2060 + args.writeString(BASE64MailboxEncoder.encode(name)); + } + + /** + * SELECT Command. + * + * @param mbox the mailbox name + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.1" + */ + public MailboxInfo select(String mbox) throws ProtocolException { + return select(mbox, null); + } + + /** + * SELECT Command with QRESYNC data. + * + * @param mbox the mailbox name + * @param rd the ResyncData + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.1" + * @see "RFC5162, section 3.1" + * @since JavaMail 1.5.1 + */ + public MailboxInfo select(String mbox, ResyncData rd) + throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + if (rd != null) { + if (rd == ResyncData.CONDSTORE) { + if (!hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + args.writeArgument(new Argument().writeAtom("CONDSTORE")); + } else { + if (!hasCapability("QRESYNC")) + throw new BadCommandException("QRESYNC not supported"); + args.writeArgument(resyncArgs(rd)); + } + } + + Response[] r = command("SELECT", args); + + // Note that MailboxInfo also removes those responses + // it knows about + MailboxInfo minfo = new MailboxInfo(r); + + // dispatch any remaining untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + + if (response.isOK()) { // command succesful + if (response.toString().indexOf("READ-ONLY") != -1) + minfo.mode = Folder.READ_ONLY; + else + minfo.mode = Folder.READ_WRITE; + } + + handleResult(response); + return minfo; + } + + /** + * EXAMINE Command. + * + * @param mbox the mailbox name + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.2" + */ + public MailboxInfo examine(String mbox) throws ProtocolException { + return examine(mbox, null); + } + + /** + * EXAMINE Command with QRESYNC data. + * + * @param mbox the mailbox name + * @param rd the ResyncData + * @return MailboxInfo if successful + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.2" + * @see "RFC5162, section 3.1" + * @since JavaMail 1.5.1 + */ + public MailboxInfo examine(String mbox, ResyncData rd) + throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + if (rd != null) { + if (rd == ResyncData.CONDSTORE) { + if (!hasCapability("CONDSTORE")) + throw new BadCommandException("CONDSTORE not supported"); + args.writeArgument(new Argument().writeAtom("CONDSTORE")); + } else { + if (!hasCapability("QRESYNC")) + throw new BadCommandException("QRESYNC not supported"); + args.writeArgument(resyncArgs(rd)); + } + } + + Response[] r = command("EXAMINE", args); + + // Note that MailboxInfo also removes those responses + // it knows about + MailboxInfo minfo = new MailboxInfo(r); + minfo.mode = Folder.READ_ONLY; // Obviously + + // dispatch any remaining untagged responses + notifyResponseHandlers(r); + + handleResult(r[r.length-1]); + return minfo; + } + + /** + * Generate a QRESYNC argument list based on the ResyncData. + */ + private static Argument resyncArgs(ResyncData rd) { + Argument cmd = new Argument(); + cmd.writeAtom("QRESYNC"); + Argument args = new Argument(); + args.writeNumber(rd.getUIDValidity()); + args.writeNumber(rd.getModSeq()); + UIDSet[] uids = Utility.getResyncUIDSet(rd); + if (uids != null) + args.writeString(UIDSet.toString(uids)); + cmd.writeArgument(args); + return cmd; + } + + /** + * ENABLE Command. + * + * @param cap the name of the capability to enable + * @exception ProtocolException for protocol failures + * @see "RFC 5161" + * @since JavaMail 1.5.1 + */ + public void enable(String cap) throws ProtocolException { + if (!hasCapability("ENABLE")) + throw new BadCommandException("ENABLE not supported"); + Argument args = new Argument(); + args.writeAtom(cap); + simpleCommand("ENABLE", args); + if (enabled == null) + enabled = new HashSet<>(); + enabled.add(cap.toUpperCase(Locale.ENGLISH)); + + // update the utf8 flag + utf8 = isEnabled("UTF8=ACCEPT"); + } + + /** + * Is the capability/extension enabled? + * + * @param cap the capability name + * @return true if enabled + * @see "RFC 5161" + * @since JavaMail 1.5.1 + */ + public boolean isEnabled(String cap) { + if (enabled == null) + return false; + else + return enabled.contains(cap.toUpperCase(Locale.ENGLISH)); + } + + /** + * UNSELECT Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC 3691" + * @since JavaMail 1.4.4 + */ + public void unselect() throws ProtocolException { + if (!hasCapability("UNSELECT")) + throw new BadCommandException("UNSELECT not supported"); + simpleCommand("UNSELECT", null); + } + + /** + * STATUS Command. + * + * @param mbox the mailbox + * @param items the STATUS items to request + * @return STATUS results + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.10" + */ + public Status status(String mbox, String[] items) + throws ProtocolException { + if (!isREV1() && !hasCapability("IMAP4SUNVERSION")) + // STATUS is rev1 only, however the non-rev1 SIMS2.0 + // does support this. + throw new BadCommandException("STATUS not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Argument itemArgs = new Argument(); + if (items == null) + items = Status.standardItems; + + for (int i = 0, len = items.length; i < len; i++) + itemArgs.writeAtom(items[i]); + args.writeArgument(itemArgs); + + Response[] r = command("STATUS", args); + + Status status = null; + Response response = r[r.length-1]; + + // Grab all STATUS responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("STATUS")) { + if (status == null) + status = new Status(ir); + else // collect 'em all + Status.add(status, new Status(ir)); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return status; + } + + /** + * CREATE Command. + * + * @param mbox the mailbox to create + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.3" + */ + public void create(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("CREATE", args); + } + + /** + * DELETE Command. + * + * @param mbox the mailbox to delete + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.4" + */ + public void delete(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("DELETE", args); + } + + /** + * RENAME Command. + * + * @param o old mailbox name + * @param n new mailbox name + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.5" + */ + public void rename(String o, String n) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, o); + writeMailboxName(args, n); + + simpleCommand("RENAME", args); + } + + /** + * SUBSCRIBE Command. + * + * @param mbox the mailbox + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.6" + */ + public void subscribe(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("SUBSCRIBE", args); + } + + /** + * UNSUBSCRIBE Command. + * + * @param mbox the mailbox + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.7" + */ + public void unsubscribe(String mbox) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + simpleCommand("UNSUBSCRIBE", args); + } + + /** + * LIST Command. + * + * @param ref reference string + * @param pattern pattern to list + * @return LIST results + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.8" + */ + public ListInfo[] list(String ref, String pattern) + throws ProtocolException { + return doList("LIST", ref, pattern); + } + + /** + * LSUB Command. + * + * @param ref reference string + * @param pattern pattern to list + * @return LSUB results + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.9" + */ + public ListInfo[] lsub(String ref, String pattern) + throws ProtocolException { + return doList("LSUB", ref, pattern); + } + + /** + * Execute the specified LIST-like command (e.g., "LIST" or "LSUB"), + * using the reference and pattern. + * + * @param cmd the list command + * @param ref the reference string + * @param pat the pattern + * @return array of ListInfo results + * @exception ProtocolException for protocol failures + * @since JavaMail 1.4.6 + */ + protected ListInfo[] doList(String cmd, String ref, String pat) + throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, ref); + writeMailboxName(args, pat); + + Response[] r = command(cmd, args); + + ListInfo[] linfo = null; + Response response = r[r.length-1]; + + if (response.isOK()) { // command succesful + List v = new ArrayList<>(1); + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals(cmd)) { + v.add(new ListInfo(ir)); + r[i] = null; + } + } + if (v.size() > 0) { + linfo = v.toArray(new ListInfo[v.size()]); + } + } + + // Dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return linfo; + } + + /** + * APPEND Command. + * + * @param mbox the mailbox + * @param f the message Flags + * @param d the message date + * @param data the message data + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.11" + */ + public void append(String mbox, Flags f, Date d, + Literal data) throws ProtocolException { + appenduid(mbox, f, d, data, false); // ignore return value + } + + /** + * APPEND Command, return uid from APPENDUID response code. + * + * @param mbox the mailbox + * @param f the message Flags + * @param d the message date + * @param data the message data + * @return APPENDUID data + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.3.11" + */ + public AppendUID appenduid(String mbox, Flags f, Date d, + Literal data) throws ProtocolException { + return appenduid(mbox, f, d, data, true); + } + + public AppendUID appenduid(String mbox, Flags f, Date d, + Literal data, boolean uid) throws ProtocolException { + Argument args = new Argument(); + writeMailboxName(args, mbox); + + if (f != null) { // set Flags in appended message + // can't set the \Recent flag in APPEND + if (f.contains(Flags.Flag.RECENT)) { + f = new Flags(f); // copy, don't modify orig + f.remove(Flags.Flag.RECENT); // remove RECENT from copy + } + + /* + * HACK ALERT: We want the flag_list to be written out + * without any checking/processing of the bytes in it. If + * I use writeString(), the flag_list will end up being + * quoted since it contains "illegal" characters. So I + * am depending on implementation knowledge that writeAtom() + * does not do any checking/processing - it just writes out + * the bytes. What we really need is a writeFoo() that just + * dumps out its argument. + */ + args.writeAtom(createFlagList(f)); + } + if (d != null) // set INTERNALDATE in appended message + args.writeString(INTERNALDATE.format(d)); + + args.writeBytes(data); + + Response[] r = command("APPEND", args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + + if (uid) + return getAppendUID(r[r.length-1]); + else + return null; + } + + /** + * If the response contains an APPENDUID response code, extract + * it and return an AppendUID object with the information. + */ + private AppendUID getAppendUID(Response r) { + if (!r.isOK()) + return null; + byte b; + while ((b = r.readByte()) > 0 && b != (byte)'[') + ; + if (b == 0) + return null; + String s; + s = r.readAtom(); + if (!s.equalsIgnoreCase("APPENDUID")) + return null; + + long uidvalidity = r.readLong(); + long uid = r.readLong(); + return new AppendUID(uidvalidity, uid); + } + + /** + * CHECK Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.4.1" + */ + public void check() throws ProtocolException { + simpleCommand("CHECK", null); + } + + /** + * CLOSE Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.4.2" + */ + public void close() throws ProtocolException { + simpleCommand("CLOSE", null); + } + + /** + * EXPUNGE Command. + * + * @exception ProtocolException for protocol failures + * @see "RFC2060, section 6.4.3" + */ + public void expunge() throws ProtocolException { + simpleCommand("EXPUNGE", null); + } + + /** + * UID EXPUNGE Command. + * + * @param set UIDs to expunge + * @exception ProtocolException for protocol failures + * @see "RFC4315, section 2" + */ + public void uidexpunge(UIDSet[] set) throws ProtocolException { + if (!hasCapability("UIDPLUS")) + throw new BadCommandException("UID EXPUNGE not supported"); + simpleCommand("UID EXPUNGE " + UIDSet.toString(set), null); + } + + /** + * Fetch the BODYSTRUCTURE of the specified message. + * + * @param msgno the message number + * @return the BODYSTRUCTURE item + * @exception ProtocolException for protocol failures + */ + public BODYSTRUCTURE fetchBodyStructure(int msgno) + throws ProtocolException { + Response[] r = fetch(msgno, "BODYSTRUCTURE"); + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, BODYSTRUCTURE.class); + else if (response.isNO()) + return null; + else { + handleResult(response); + return null; + } + } + + /** + * Fetch given BODY section, without marking the message + * as SEEN. + * + * @param msgno the message number + * @param section the body section + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY peekBody(int msgno, String section) + throws ProtocolException { + return fetchBody(msgno, section, true); + } + + /** + * Fetch given BODY section. + * + * @param msgno the message number + * @param section the body section + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY fetchBody(int msgno, String section) + throws ProtocolException { + return fetchBody(msgno, section, false); + } + + protected BODY fetchBody(int msgno, String section, boolean peek) + throws ProtocolException { + Response[] r; + + if (section == null) + section = ""; + String body = (peek ? "BODY.PEEK[" : "BODY[") + section + "]"; + return fetchSectionBody(msgno, section, body); + } + + /** + * Partial FETCH of given BODY section, without setting SEEN flag. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY peekBody(int msgno, String section, int start, int size) + throws ProtocolException { + return fetchBody(msgno, section, start, size, true, null); + } + + /** + * Partial FETCH of given BODY section. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY fetchBody(int msgno, String section, int start, int size) + throws ProtocolException { + return fetchBody(msgno, section, start, size, false, null); + } + + /** + * Partial FETCH of given BODY section, without setting SEEN flag. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @param ba the buffer into which to read the response + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY peekBody(int msgno, String section, int start, int size, + ByteArray ba) throws ProtocolException { + return fetchBody(msgno, section, start, size, true, ba); + } + + /** + * Partial FETCH of given BODY section. + * + * @param msgno the message number + * @param section the body section + * @param start starting byte count + * @param size number of bytes to fetch + * @param ba the buffer into which to read the response + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + public BODY fetchBody(int msgno, String section, int start, int size, + ByteArray ba) throws ProtocolException { + return fetchBody(msgno, section, start, size, false, ba); + } + + protected BODY fetchBody(int msgno, String section, int start, int size, + boolean peek, ByteArray ba) throws ProtocolException { + this.ba = ba; // save for later use by getResponseBuffer + if (section == null) + section = ""; + String body = (peek ? "BODY.PEEK[" : "BODY[") + section + "]<" + + String.valueOf(start) + "." + + String.valueOf(size) + ">"; + return fetchSectionBody(msgno, section, body); + } + + /** + * Fetch the given body section of the given message, using the + * body string "body". + * + * @param msgno the message number + * @param section the body section + * @param body the body string + * @return the BODY item + * @exception ProtocolException for protocol failures + */ + protected BODY fetchSectionBody(int msgno, String section, String body) + throws ProtocolException { + Response[] r; + + r = fetch(msgno, body); + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) { + List bl = FetchResponse.getItems(r, msgno, BODY.class); + if (bl.size() == 1) + return bl.get(0); // the common case + if (logger.isLoggable(Level.FINEST)) + logger.finest("got " + bl.size() + + " BODY responses for section " + section); + // more then one BODY response, have to find the right one + for (BODY br : bl) { + if (logger.isLoggable(Level.FINEST)) + logger.finest("got BODY section " + br.getSection()); + if (br.getSection().equalsIgnoreCase(section)) + return br; // that's the one! + } + return null; // couldn't find it + } else if (response.isNO()) + return null; + else { + handleResult(response); + return null; + } + } + + /** + * Return a buffer to read a response into. + * The buffer is provided by fetchBody and is + * used only once. + * + * @return the buffer to use + */ + @Override + protected ByteArray getResponseBuffer() { + ByteArray ret = ba; + ba = null; + return ret; + } + + /** + * Fetch the specified RFC822 Data item. 'what' names + * the item to be fetched. 'what' can be null + * to fetch the whole message. + * + * @param msgno the message number + * @param what the item to fetch + * @return the RFC822DATA item + * @exception ProtocolException for protocol failures + */ + public RFC822DATA fetchRFC822(int msgno, String what) + throws ProtocolException { + Response[] r = fetch(msgno, + what == null ? "RFC822" : "RFC822." + what + ); + + // dispatch untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, RFC822DATA.class); + else if (response.isNO()) + return null; + else { + handleResult(response); + return null; + } + } + + /** + * Fetch the FLAGS for the given message. + * + * @param msgno the message number + * @return the Flags + * @exception ProtocolException for protocol failures + */ + public Flags fetchFlags(int msgno) throws ProtocolException { + Flags flags = null; + Response[] r = fetch(msgno, "FLAGS"); + + // Search for our FLAGS response + for (int i = 0, len = r.length; i < len; i++) { + if (r[i] == null || + !(r[i] instanceof FetchResponse) || + ((FetchResponse)r[i]).getNumber() != msgno) + continue; + + FetchResponse fr = (FetchResponse)r[i]; + if ((flags = fr.getItem(FLAGS.class)) != null) { + r[i] = null; // remove this response + break; + } + } + + // dispatch untagged responses + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + return flags; + } + + /** + * Fetch the IMAP UID for the given message. + * + * @param msgno the message number + * @return the UID + * @exception ProtocolException for protocol failures + */ + public UID fetchUID(int msgno) throws ProtocolException { + Response[] r = fetch(msgno, "UID"); + + // dispatch untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, UID.class); + else if (response.isNO()) // XXX: Issue NOOP ? + return null; + else { + handleResult(response); + return null; // NOTREACHED + } + } + + /** + * Fetch the IMAP MODSEQ for the given message. + * + * @param msgno the message number + * @return the MODSEQ + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.1 + */ + public MODSEQ fetchMODSEQ(int msgno) throws ProtocolException { + Response[] r = fetch(msgno, "MODSEQ"); + + // dispatch untagged responses + notifyResponseHandlers(r); + + Response response = r[r.length-1]; + if (response.isOK()) + return FetchResponse.getItem(r, msgno, MODSEQ.class); + else if (response.isNO()) // XXX: Issue NOOP ? + return null; + else { + handleResult(response); + return null; // NOTREACHED + } + } + + /** + * Get the sequence number for the given UID. Nothing is returned; + * the FETCH UID response must be handled by the reponse handler, + * along with any possible EXPUNGE responses, to ensure that the + * UID is matched with the correct sequence number. + * + * @param uid the UID + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.3 + */ + public void fetchSequenceNumber(long uid) throws ProtocolException { + Response[] r = fetch(String.valueOf(uid), "UID", true); + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + } + + /** + * Get the sequence numbers for UIDs ranging from start till end. + * Since the range may be large and sparse, an array of the UIDs actually + * found is returned. The caller must map these to messages after + * the FETCH UID responses have been handled by the reponse handler, + * along with any possible EXPUNGE responses, to ensure that the + * UIDs are matched with the correct sequence numbers. + * + * @param start first UID + * @param end last UID + * @return array of sequence numbers + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.3 + */ + public long[] fetchSequenceNumbers(long start, long end) + throws ProtocolException { + Response[] r = fetch(String.valueOf(start) + ":" + + (end == UIDFolder.LASTUID ? "*" : + String.valueOf(end)), + "UID", true); + + UID u; + List v = new ArrayList<>(); + for (int i = 0, len = r.length; i < len; i++) { + if (r[i] == null || !(r[i] instanceof FetchResponse)) + continue; + + FetchResponse fr = (FetchResponse)r[i]; + if ((u = fr.getItem(UID.class)) != null) + v.add(u); + } + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + + long[] lv = new long[v.size()]; + for (int i = 0; i < v.size(); i++) + lv[i] = v.get(i).uid; + return lv; + } + + /** + * Get the sequence numbers for UIDs specified in the array. + * Nothing is returned. The caller must map the UIDs to messages after + * the FETCH UID responses have been handled by the reponse handler, + * along with any possible EXPUNGE responses, to ensure that the + * UIDs are matched with the correct sequence numbers. + * + * @param uids the UIDs + * @exception ProtocolException for protocol failures + * @since JavaMail 1.5.3 + */ + public void fetchSequenceNumbers(long[] uids) throws ProtocolException { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < uids.length; i++) { + if (i > 0) + sb.append(","); + sb.append(String.valueOf(uids[i])); + } + + Response[] r = fetch(sb.toString(), "UID", true); + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + } + + /** + * Get the sequence numbers for messages changed since the given + * modseq and with UIDs ranging from start till end. + * Also, prefetch the flags for the returned messages. + * + * @param start first UID + * @param end last UID + * @param modseq the MODSEQ + * @return array of sequence numbers + * @exception ProtocolException for protocol failures + * @see "RFC 4551" + * @since JavaMail 1.5.1 + */ + public int[] uidfetchChangedSince(long start, long end, long modseq) + throws ProtocolException { + String msgSequence = String.valueOf(start) + ":" + + (end == UIDFolder.LASTUID ? "*" : + String.valueOf(end)); + Response[] r = command("UID FETCH " + msgSequence + + " (FLAGS) (CHANGEDSINCE " + String.valueOf(modseq) + ")", null); + + List v = new ArrayList<>(); + for (int i = 0, len = r.length; i < len; i++) { + if (r[i] == null || !(r[i] instanceof FetchResponse)) + continue; + + FetchResponse fr = (FetchResponse)r[i]; + v.add(Integer.valueOf(fr.getNumber())); + } + + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + + // Copy the list into 'matches' + int vsize = v.size(); + int[] matches = new int[vsize]; + for (int i = 0; i < vsize; i++) + matches[i] = v.get(i).intValue(); + return matches; + } + + public Response[] fetch(MessageSet[] msgsets, String what) + throws ProtocolException { + return fetch(MessageSet.toString(msgsets), what, false); + } + + public Response[] fetch(int start, int end, String what) + throws ProtocolException { + return fetch(String.valueOf(start) + ":" + String.valueOf(end), + what, false); + } + + public Response[] fetch(int msg, String what) + throws ProtocolException { + return fetch(String.valueOf(msg), what, false); + } + + private Response[] fetch(String msgSequence, String what, boolean uid) + throws ProtocolException { + if (uid) + return command("UID FETCH " + msgSequence +" (" + what + ")",null); + else + return command("FETCH " + msgSequence + " (" + what + ")", null); + } + + /** + * COPY command. + * + * @param msgsets the messages to copy + * @param mbox the mailbox to copy them to + * @exception ProtocolException for protocol failures + */ + public void copy(MessageSet[] msgsets, String mbox) + throws ProtocolException { + copyuid(MessageSet.toString(msgsets), mbox, false); + } + + /** + * COPY command. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to copy them to + * @exception ProtocolException for protocol failures + */ + public void copy(int start, int end, String mbox) + throws ProtocolException { + copyuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, false); + } + + /** + * COPY command, return uid from COPYUID response code. + * + * @param msgsets the messages to copy + * @param mbox the mailbox to copy them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 4315, section 3" + */ + public CopyUID copyuid(MessageSet[] msgsets, String mbox) + throws ProtocolException { + return copyuid(MessageSet.toString(msgsets), mbox, true); + } + + /** + * COPY command, return uid from COPYUID response code. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to copy them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 4315, section 3" + */ + public CopyUID copyuid(int start, int end, String mbox) + throws ProtocolException { + return copyuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, true); + } + + private CopyUID copyuid(String msgSequence, String mbox, boolean uid) + throws ProtocolException { + if (uid && !hasCapability("UIDPLUS")) + throw new BadCommandException("UIDPLUS not supported"); + + Argument args = new Argument(); + args.writeAtom(msgSequence); + writeMailboxName(args, mbox); + + Response[] r = command("COPY", args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + + if (uid) + return getCopyUID(r); + else + return null; + } + + /** + * MOVE command. + * + * @param msgsets the messages to move + * @param mbox the mailbox to move them to + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @since JavaMail 1.5.4 + */ + public void move(MessageSet[] msgsets, String mbox) + throws ProtocolException { + moveuid(MessageSet.toString(msgsets), mbox, false); + } + + /** + * MOVE command. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to move them to + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @since JavaMail 1.5.4 + */ + public void move(int start, int end, String mbox) + throws ProtocolException { + moveuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, false); + } + + /** + * MOVE Command, return uid from COPYUID response code. + * + * @param msgsets the messages to move + * @param mbox the mailbox to move them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @see "RFC 4315, section 3" + * @since JavaMail 1.5.4 + */ + public CopyUID moveuid(MessageSet[] msgsets, String mbox) + throws ProtocolException { + return moveuid(MessageSet.toString(msgsets), mbox, true); + } + + /** + * MOVE Command, return uid from COPYUID response code. + * + * @param start start message number + * @param end end message number + * @param mbox the mailbox to move them to + * @return COPYUID response data + * @exception ProtocolException for protocol failures + * @see "RFC 6851" + * @see "RFC 4315, section 3" + * @since JavaMail 1.5.4 + */ + public CopyUID moveuid(int start, int end, String mbox) + throws ProtocolException { + return moveuid(String.valueOf(start) + ":" + String.valueOf(end), + mbox, true); + } + + /** + * MOVE Command, return uid from COPYUID response code. + * + * @see "RFC 6851" + * @see "RFC 4315, section 3" + * @since JavaMail 1.5.4 + */ + private CopyUID moveuid(String msgSequence, String mbox, boolean uid) + throws ProtocolException { + if (!hasCapability("MOVE")) + throw new BadCommandException("MOVE not supported"); + if (uid && !hasCapability("UIDPLUS")) + throw new BadCommandException("UIDPLUS not supported"); + + Argument args = new Argument(); + args.writeAtom(msgSequence); + writeMailboxName(args, mbox); + + Response[] r = command("MOVE", args); + + // dispatch untagged responses + notifyResponseHandlers(r); + + // Handle result of this command + handleResult(r[r.length-1]); + + if (uid) + return getCopyUID(r); + else + return null; + } + + /** + * If the response contains a COPYUID response code, extract + * it and return a CopyUID object with the information. + * + * @param rr the responses to examine + * @return the COPYUID response code data, or null if not found + * @since JavaMail 1.5.4 + */ + protected CopyUID getCopyUID(Response[] rr) { + // most likely in the last response, so start there and work backward + for (int i = rr.length - 1; i >= 0; i--) { + Response r = rr[i]; + if (r == null || !r.isOK()) + continue; + byte b; + while ((b = r.readByte()) > 0 && b != (byte)'[') + ; + if (b == 0) + continue; + String s; + s = r.readAtom(); + if (!s.equalsIgnoreCase("COPYUID")) + continue; + + // XXX - need to merge more than one response for MOVE? + long uidvalidity = r.readLong(); + String src = r.readAtom(); + String dst = r.readAtom(); + return new CopyUID(uidvalidity, + UIDSet.parseUIDSets(src), UIDSet.parseUIDSets(dst)); + } + return null; + } + + public void storeFlags(MessageSet[] msgsets, Flags flags, boolean set) + throws ProtocolException { + storeFlags(MessageSet.toString(msgsets), flags, set); + } + + public void storeFlags(int start, int end, Flags flags, boolean set) + throws ProtocolException { + storeFlags(String.valueOf(start) + ":" + String.valueOf(end), + flags, set); + } + + /** + * Set the specified flags on this message. + * + * @param msg the message number + * @param flags the flags + * @param set true to set, false to clear + * @exception ProtocolException for protocol failures + */ + public void storeFlags(int msg, Flags flags, boolean set) + throws ProtocolException { + storeFlags(String.valueOf(msg), flags, set); + } + + private void storeFlags(String msgset, Flags flags, boolean set) + throws ProtocolException { + Response[] r; + if (set) + r = command("STORE " + msgset + " +FLAGS " + + createFlagList(flags), null); + else + r = command("STORE " + msgset + " -FLAGS " + + createFlagList(flags), null); + + // Dispatch untagged responses + notifyResponseHandlers(r); + handleResult(r[r.length-1]); + } + + /** + * Creates an IMAP flag_list from the given Flags object. + * + * @param flags the flags + * @return the IMAP flag_list + * @since JavaMail 1.5.4 + */ + protected String createFlagList(Flags flags) { + StringBuilder sb = new StringBuilder("("); // start of flag_list + + Flags.Flag[] sf = flags.getSystemFlags(); // get the system flags + boolean first = true; + for (int i = 0; i < sf.length; i++) { + String s; + Flags.Flag f = sf[i]; + if (f == Flags.Flag.ANSWERED) + s = "\\Answered"; + else if (f == Flags.Flag.DELETED) + s = "\\Deleted"; + else if (f == Flags.Flag.DRAFT) + s = "\\Draft"; + else if (f == Flags.Flag.FLAGGED) + s = "\\Flagged"; + else if (f == Flags.Flag.RECENT) + s = "\\Recent"; + else if (f == Flags.Flag.SEEN) + s = "\\Seen"; + else + continue; // skip it + if (first) + first = false; + else + sb.append(' '); + sb.append(s); + } + + String[] uf = flags.getUserFlags(); // get the user flag strings + for (int i = 0; i < uf.length; i++) { + if (first) + first = false; + else + sb.append(' '); + sb.append(uf[i]); + } + + sb.append(")"); // terminate flag_list + return sb.toString(); + } + + /** + * Issue the given search criterion on the specified message sets. + * Returns array of matching sequence numbers. An empty array + * is returned if no matches are found. + * + * @param msgsets array of MessageSets + * @param term SearchTerm + * @return array of matching sequence numbers. + * @exception ProtocolException for protocol failures + * @exception SearchException for search failures + */ + public int[] search(MessageSet[] msgsets, SearchTerm term) + throws ProtocolException, SearchException { + return search(MessageSet.toString(msgsets), term); + } + + /** + * Issue the given search criterion on all messages in this folder. + * Returns array of matching sequence numbers. An empty array + * is returned if no matches are found. + * + * @param term SearchTerm + * @return array of matching sequence numbers. + * @exception ProtocolException for protocol failures + * @exception SearchException for search failures + */ + public int[] search(SearchTerm term) + throws ProtocolException, SearchException { + return search("ALL", term); + } + + /* + * Apply the given SearchTerm on the specified sequence. + * Returns array of matching sequence numbers. Note that an empty + * array is returned for no matches. + */ + private int[] search(String msgSequence, SearchTerm term) + throws ProtocolException, SearchException { + // Check if the search "text" terms contain only ASCII chars, + // or if utf8 support has been enabled (in which case CHARSET + // is not allowed; see RFC 6855, section 3, last paragraph) + if (supportsUtf8() || SearchSequence.isAscii(term)) { + try { + return issueSearch(msgSequence, term, null); + } catch (IOException ioex) { /* will not happen */ } + } + + /* + * The search "text" terms do contain non-ASCII chars and utf8 + * support has not been enabled. We need to use: + * "SEARCH CHARSET ..." + * The charsets we try to use are UTF-8 and the locale's + * default charset. If the server supports UTF-8, great, + * always use it. Else we try to use the default charset. + */ + + // Cycle thru the list of charsets + for (int i = 0; i < searchCharsets.length; i++) { + if (searchCharsets[i] == null) + continue; + + try { + return issueSearch(msgSequence, term, searchCharsets[i]); + } catch (CommandFailedException cfx) { + /* + * Server returned NO. For now, I'll just assume that + * this indicates that this charset is unsupported. + * We can check the BADCHARSET response code once + * that's spec'd into the IMAP RFC .. + */ + searchCharsets[i] = null; + continue; + } catch (IOException ioex) { + /* Charset conversion failed. Try the next one */ + continue; + } catch (ProtocolException pex) { + throw pex; + } catch (SearchException sex) { + throw sex; + } + } + + // No luck. + throw new SearchException("Search failed"); + } + + /* Apply the given SearchTerm on the specified sequence, using the + * given charset.

+ * Returns array of matching sequence numbers. Note that an empty + * array is returned for no matches. + */ + private int[] issueSearch(String msgSequence, SearchTerm term, + String charset) + throws ProtocolException, SearchException, IOException { + + // Generate a search-sequence with the given charset + Argument args = getSearchSequence().generateSequence(term, + charset == null ? null : + MimeUtility.javaCharset(charset) + ); + args.writeAtom(msgSequence); + + Response[] r; + + if (charset == null) // text is all US-ASCII + r = command("SEARCH", args); + else + r = command("SEARCH CHARSET " + charset, args); + + Response response = r[r.length-1]; + int[] matches = null; + + // Grab all SEARCH responses + if (response.isOK()) { // command succesful + List v = new ArrayList<>(); + int num; + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + // There *will* be one SEARCH response. + if (ir.keyEquals("SEARCH")) { + while ((num = ir.readNumber()) != -1) + v.add(Integer.valueOf(num)); + r[i] = null; + } + } + + // Copy the list into 'matches' + int vsize = v.size(); + matches = new int[vsize]; + for (int i = 0; i < vsize; i++) + matches[i] = v.get(i).intValue(); + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return matches; + } + + /** + * Get the SearchSequence object. + * The SearchSequence object instance is saved in the searchSequence + * field. Subclasses of IMAPProtocol may override this method to + * return a subclass of SearchSequence, in order to add support for + * product-specific search terms. + * + * @return the SearchSequence + * @since JavaMail 1.4.6 + */ + protected SearchSequence getSearchSequence() { + if (searchSequence == null) + searchSequence = new SearchSequence(this); + return searchSequence; + } + + /** + * Sort messages in the folder according to the specified sort criteria. + * If the search term is not null, limit the sort to only the messages + * that match the search term. + * Returns an array of sorted sequence numbers. An empty array + * is returned if no matches are found. + * + * @param term sort criteria + * @param sterm SearchTerm + * @return array of matching sequence numbers. + * @exception ProtocolException for protocol failures + * @exception SearchException for search failures + * + * @see "RFC 5256" + * @since JavaMail 1.4.4 + */ + public int[] sort(SortTerm[] term, SearchTerm sterm) + throws ProtocolException, SearchException { + if (!hasCapability("SORT*")) + throw new BadCommandException("SORT not supported"); + + if (term == null || term.length == 0) + throw new BadCommandException("Must have at least one sort term"); + + Argument args = new Argument(); + Argument sargs = new Argument(); + for (int i = 0; i < term.length; i++) + sargs.writeAtom(term[i].toString()); + args.writeArgument(sargs); // sort criteria + + args.writeAtom("UTF-8"); // charset specification + if (sterm != null) { + try { + args.append( + getSearchSequence().generateSequence(sterm, "UTF-8")); + } catch (IOException ioex) { + // should never happen + throw new SearchException(ioex.toString()); + } + } else + args.writeAtom("ALL"); + + Response[] r = command("SORT", args); + Response response = r[r.length-1]; + int[] matches = null; + + // Grab all SORT responses + if (response.isOK()) { // command succesful + List v = new ArrayList<>(); + int num; + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("SORT")) { + while ((num = ir.readNumber()) != -1) + v.add(Integer.valueOf(num)); + r[i] = null; + } + } + + // Copy the list into 'matches' + int vsize = v.size(); + matches = new int[vsize]; + for (int i = 0; i < vsize; i++) + matches[i] = v.get(i).intValue(); + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return matches; + } + + /** + * NAMESPACE Command. + * + * @return the namespaces + * @exception ProtocolException for protocol failures + * @see "RFC2342" + */ + public Namespaces namespace() throws ProtocolException { + if (!hasCapability("NAMESPACE")) + throw new BadCommandException("NAMESPACE not supported"); + + Response[] r = command("NAMESPACE", null); + + Namespaces namespace = null; + Response response = r[r.length-1]; + + // Grab NAMESPACE response + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("NAMESPACE")) { + if (namespace == null) + namespace = new Namespaces(ir); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return namespace; + } + + /** + * GETQUOTAROOT Command. + * + * Returns an array of Quota objects, representing the quotas + * for this mailbox and, indirectly, the quotaroots for this + * mailbox. + * + * @param mbox the mailbox + * @return array of Quota objects + * @exception ProtocolException for protocol failures + * @see "RFC2087" + */ + public Quota[] getQuotaRoot(String mbox) throws ProtocolException { + if (!hasCapability("QUOTA")) + throw new BadCommandException("GETQUOTAROOT not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Response[] r = command("GETQUOTAROOT", args); + + Response response = r[r.length-1]; + + Map tab = new HashMap<>(); + + // Grab all QUOTAROOT and QUOTA responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("QUOTAROOT")) { + // quotaroot_response + // ::= "QUOTAROOT" SP astring *(SP astring) + + // read name of mailbox and throw away + ir.readAtomString(); + // for each quotaroot add a placeholder quota + String root = null; + while ((root = ir.readAtomString()) != null && + root.length() > 0) + tab.put(root, new Quota(root)); + r[i] = null; + } else if (ir.keyEquals("QUOTA")) { + Quota quota = parseQuota(ir); + Quota q = tab.get(quota.quotaRoot); + if (q != null && q.resources != null) { + // merge resources + int newl = q.resources.length + quota.resources.length; + Quota.Resource[] newr = new Quota.Resource[newl]; + System.arraycopy(q.resources, 0, newr, 0, + q.resources.length); + System.arraycopy(quota.resources, 0, + newr, q.resources.length, quota.resources.length); + quota.resources = newr; + } + tab.put(quota.quotaRoot, quota); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + + return tab.values().toArray(new Quota[tab.size()]); + } + + /** + * GETQUOTA Command. + * + * Returns an array of Quota objects, representing the quotas + * for this quotaroot. + * + * @param root the quotaroot + * @return the quotas + * @exception ProtocolException for protocol failures + * @see "RFC2087" + */ + public Quota[] getQuota(String root) throws ProtocolException { + if (!hasCapability("QUOTA")) + throw new BadCommandException("QUOTA not supported"); + + Argument args = new Argument(); + args.writeString(root); // XXX - could be UTF-8? + + Response[] r = command("GETQUOTA", args); + + Quota quota = null; + List v = new ArrayList<>(); + Response response = r[r.length-1]; + + // Grab all QUOTA responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("QUOTA")) { + quota = parseQuota(ir); + v.add(quota); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return v.toArray(new Quota[v.size()]); + } + + /** + * SETQUOTA Command. + * + * Set the indicated quota on the corresponding quotaroot. + * + * @param quota the quota to set + * @exception ProtocolException for protocol failures + * @see "RFC2087" + */ + public void setQuota(Quota quota) throws ProtocolException { + if (!hasCapability("QUOTA")) + throw new BadCommandException("QUOTA not supported"); + + Argument args = new Argument(); + args.writeString(quota.quotaRoot); // XXX - could be UTF-8? + Argument qargs = new Argument(); + if (quota.resources != null) { + for (int i = 0; i < quota.resources.length; i++) { + qargs.writeAtom(quota.resources[i].name); + qargs.writeNumber(quota.resources[i].limit); + } + } + args.writeArgument(qargs); + + Response[] r = command("SETQUOTA", args); + Response response = r[r.length-1]; + + // XXX - It's not clear from the RFC whether the SETQUOTA command + // will provoke untagged QUOTA responses. If it does, perhaps + // we should grab them here and return them? + + /* + Quota quota = null; + List v = new ArrayList(); + + // Grab all QUOTA responses + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("QUOTA")) { + quota = parseQuota(ir); + v.add(quota); + r[i] = null; + } + } + } + */ + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + /* + return v.toArray(new Quota[v.size()]); + */ + } + + /** + * Parse a QUOTA response. + */ + private Quota parseQuota(Response r) throws ParsingException { + // quota_response ::= "QUOTA" SP astring SP quota_list + String quotaRoot = r.readAtomString(); // quotaroot ::= astring + Quota q = new Quota(quotaRoot); + r.skipSpaces(); + // quota_list ::= "(" #quota_resource ")" + if (r.readByte() != '(') + throw new ParsingException("parse error in QUOTA"); + + List v = new ArrayList<>(); + while (!r.isNextNonSpace(')')) { + // quota_resource ::= atom SP number SP number + String name = r.readAtom(); + if (name != null) { + long usage = r.readLong(); + long limit = r.readLong(); + Quota.Resource res = new Quota.Resource(name, usage, limit); + v.add(res); + } + } + q.resources = v.toArray(new Quota.Resource[v.size()]); + return q; + } + + + /** + * SETACL Command. + * + * @param mbox the mailbox + * @param modifier the ACL modifier + * @param acl the ACL + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public void setACL(String mbox, char modifier, ACL acl) + throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + args.writeString(acl.getName()); + String rights = acl.getRights().toString(); + if (modifier == '+' || modifier == '-') + rights = modifier + rights; + args.writeString(rights); + + Response[] r = command("SETACL", args); + Response response = r[r.length-1]; + + // dispatch untagged responses + notifyResponseHandlers(r); + handleResult(response); + } + + /** + * DELETEACL Command. + * + * @param mbox the mailbox + * @param user the user + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public void deleteACL(String mbox, String user) throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + args.writeString(user); // XXX - could be UTF-8? + + Response[] r = command("DELETEACL", args); + Response response = r[r.length-1]; + + // dispatch untagged responses + notifyResponseHandlers(r); + handleResult(response); + } + + /** + * GETACL Command. + * + * @param mbox the mailbox + * @return the ACL array + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public ACL[] getACL(String mbox) throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Response[] r = command("GETACL", args); + Response response = r[r.length-1]; + + // Grab all ACL responses + List v = new ArrayList<>(); + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("ACL")) { + // acl_data ::= "ACL" SPACE mailbox + // *(SPACE identifier SPACE rights) + // read name of mailbox and throw away + ir.readAtomString(); + String name = null; + while ((name = ir.readAtomString()) != null) { + String rights = ir.readAtomString(); + if (rights == null) + break; + ACL acl = new ACL(name, new Rights(rights)); + v.add(acl); + } + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return v.toArray(new ACL[v.size()]); + } + + /** + * LISTRIGHTS Command. + * + * @param mbox the mailbox + * @param user the user rights to return + * @return the rights array + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public Rights[] listRights(String mbox, String user) + throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + args.writeString(user); // XXX - could be UTF-8? + + Response[] r = command("LISTRIGHTS", args); + Response response = r[r.length-1]; + + // Grab LISTRIGHTS response + List v = new ArrayList<>(); + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("LISTRIGHTS")) { + // listrights_data ::= "LISTRIGHTS" SPACE mailbox + // SPACE identifier SPACE rights *(SPACE rights) + // read name of mailbox and throw away + ir.readAtomString(); + // read identifier and throw away + ir.readAtomString(); + String rights; + while ((rights = ir.readAtomString()) != null) + v.add(new Rights(rights)); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return v.toArray(new Rights[v.size()]); + } + + /** + * MYRIGHTS Command. + * + * @param mbox the mailbox + * @return the rights + * @exception ProtocolException for protocol failures + * @see "RFC2086" + */ + public Rights myRights(String mbox) throws ProtocolException { + if (!hasCapability("ACL")) + throw new BadCommandException("ACL not supported"); + + Argument args = new Argument(); + writeMailboxName(args, mbox); + + Response[] r = command("MYRIGHTS", args); + Response response = r[r.length-1]; + + // Grab MYRIGHTS response + Rights rights = null; + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("MYRIGHTS")) { + // myrights_data ::= "MYRIGHTS" SPACE mailbox SPACE rights + // read name of mailbox and throw away + ir.readAtomString(); + String rs = ir.readAtomString(); + if (rights == null) + rights = new Rights(rs); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return rights; + } + + /* + * The tag used on the IDLE command. Set by idleStart() and + * used in processIdleResponse() to determine if the response + * is the matching end tag. + */ + private volatile String idleTag; + + /** + * IDLE Command.

+ * + * If the server supports the IDLE command extension, the IDLE + * command is issued and this method blocks until a response has + * been received. Once the first response has been received, the + * IDLE command is terminated and all responses are collected and + * handled and this method returns.

+ * + * Note that while this method is blocked waiting for a response, + * no other threads may issue any commands to the server that would + * use this same connection. + * + * @exception ProtocolException for protocol failures + * @see "RFC2177" + * @since JavaMail 1.4.1 + */ + public synchronized void idleStart() throws ProtocolException { + if (!hasCapability("IDLE")) + throw new BadCommandException("IDLE not supported"); + + List v = new ArrayList<>(); + boolean done = false; + Response r = null; + + // write the command + try { + idleTag = writeCommand("IDLE", null); + } catch (LiteralException lex) { + v.add(lex.getResponse()); + done = true; + } catch (Exception ex) { + // Convert this into a BYE response + v.add(Response.byeResponse(ex)); + done = true; + } + + while (!done) { + try { + r = readResponse(); + } catch (IOException ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + } catch (ProtocolException pex) { + continue; // skip this response + } + + v.add(r); + + if (r.isContinuation() || r.isBYE()) + done = true; + } + + Response[] responses = v.toArray(new Response[v.size()]); + r = responses[responses.length-1]; + + // dispatch remaining untagged responses + notifyResponseHandlers(responses); + if (!r.isContinuation()) + handleResult(r); + } + + /** + * While an IDLE command is in progress, read a response + * sent from the server. The response is read with no locks + * held so that when the read blocks waiting for the response + * from the server it's not holding locks that would prevent + * other threads from interrupting the IDLE command. + * + * @return the response + * @since JavaMail 1.4.1 + */ + public synchronized Response readIdleResponse() { + if (idleTag == null) + return null; // IDLE not in progress + Response r = null; + try { + r = readResponse(); + } catch (IOException ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + } catch (ProtocolException pex) { + // convert this into a BYE response + r = Response.byeResponse(pex); + } + return r; + } + + /** + * Process a response returned by readIdleResponse(). + * This method will be called with appropriate locks + * held so that the processing of the response is safe. + * + * @param r the response + * @return true if IDLE is done + * @exception ProtocolException for protocol failures + * @since JavaMail 1.4.1 + */ + public boolean processIdleResponse(Response r) throws ProtocolException { + Response[] responses = new Response[1]; + responses[0] = r; + boolean done = false; // done reading responses? + notifyResponseHandlers(responses); + + if (r.isBYE()) // shouldn't wait for command completion response + done = true; + + // If this is a matching command completion response, we are done + if (r.isTagged() && r.getTag().equals(idleTag)) + done = true; + + if (done) + idleTag = null; // no longer in IDLE + + handleResult(r); + return !done; + } + + // the DONE command to break out of IDLE + private static final byte[] DONE = { 'D', 'O', 'N', 'E', '\r', '\n' }; + + /** + * Abort an IDLE command. While one thread is blocked in + * readIdleResponse(), another thread will use this method + * to abort the IDLE command, which will cause the server + * to send the closing tag for the IDLE command, which + * readIdleResponse() and processIdleResponse() will see + * and terminate the IDLE state. + * + * @since JavaMail 1.4.1 + */ + public void idleAbort() { + OutputStream os = getOutputStream(); + try { + os.write(DONE); + os.flush(); + } catch (Exception ex) { + // nothing to do, hope to detect it again later + logger.log(Level.FINEST, "Exception aborting IDLE", ex); + } + } + + /** + * ID Command. + * + * @param clientParams map of names and values + * @return map of names and values from server + * @exception ProtocolException for protocol failures + * @see "RFC 2971" + * @since JavaMail 1.5.1 + */ + public Map id(Map clientParams) + throws ProtocolException { + if (!hasCapability("ID")) + throw new BadCommandException("ID not supported"); + + Response[] r = command("ID", ID.getArgumentList(clientParams)); + + ID id = null; + Response response = r[r.length-1]; + + // Grab ID response + if (response.isOK()) { // command succesful + for (int i = 0, len = r.length; i < len; i++) { + if (!(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + if (ir.keyEquals("ID")) { + if (id == null) + id = new ID(ir); + r[i] = null; + } + } + } + + // dispatch remaining untagged responses + notifyResponseHandlers(r); + handleResult(response); + return id == null ? null : id.getServerParams(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/IMAPReferralException.java b/app/src/main/java/com/sun/mail/imap/protocol/IMAPReferralException.java new file mode 100644 index 0000000000..82f1ea39d1 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/IMAPReferralException.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.ProtocolException; + +/** + * A ProtocolException that includes IMAP login referral information. + * + * @since JavaMail 1.5.5 + */ + +public class IMAPReferralException extends ProtocolException { + + private String url; + + private static final long serialVersionUID = 2578770669364251968L; + + /** + * Constructs an IMAPReferralException with the specified detail message. + * and URL. + * + * @param s the detail message + * @param url the URL + */ + public IMAPReferralException(String s, String url) { + super(s); + this.url = url; + } + + /** + * Return the IMAP URL in the referral. + * + * @return the IMAP URL + */ + public String getUrl() { + return url; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/IMAPResponse.java b/app/src/main/java/com/sun/mail/imap/protocol/IMAPResponse.java new file mode 100644 index 0000000000..4e6be4e1b3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/IMAPResponse.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.*; +import java.util.*; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.iap.*; + +/** + * This class represents a response obtained from the input stream + * of an IMAP server. + * + * @author John Mani + */ + +public class IMAPResponse extends Response { + private String key; + private int number; + + public IMAPResponse(Protocol c) throws IOException, ProtocolException { + super(c); + init(); + } + + private void init() throws IOException, ProtocolException { + // continue parsing if this is an untagged response + if (isUnTagged() && !isOK() && !isNO() && !isBAD() && !isBYE()) { + key = readAtom(); + + // Is this response of the form "* " + try { + number = Integer.parseInt(key); + key = readAtom(); + } catch (NumberFormatException ne) { } + } + } + + /** + * Copy constructor. + * + * @param r the IMAPResponse to copy + */ + public IMAPResponse(IMAPResponse r) { + super((Response)r); + key = r.key; + number = r.number; + } + + /** + * For testing. + * + * @param r the response string + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + public IMAPResponse(String r) throws IOException, ProtocolException { + this(r, true); + } + + /** + * For testing. + * + * @param r the response string + * @param utf8 UTF-8 allowed? + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + * @since JavaMail 1.6.0 + */ + public IMAPResponse(String r, boolean utf8) + throws IOException, ProtocolException { + super(r, utf8); + init(); + } + + /** + * Read a list of space-separated "flag-extension" sequences and + * return the list as a array of Strings. An empty list is returned + * as null. Each item is expected to be an atom, possibly preceeded + * by a backslash, but we aren't that strict; we just look for strings + * separated by spaces and terminated by a right paren. We assume items + * are always ASCII. + * + * @return the list items as a String array + */ + public String[] readSimpleList() { + skipSpaces(); + + if (buffer[index] != '(') // not what we expected + return null; + index++; // skip '(' + + List v = new ArrayList<>(); + int start; + for (start = index; buffer[index] != ')'; index++) { + if (buffer[index] == ' ') { // got one item + v.add(ASCIIUtility.toString(buffer, start, index)); + start = index+1; // index gets incremented at the top + } + } + if (index > start) // get the last item + v.add(ASCIIUtility.toString(buffer, start, index)); + index++; // skip ')' + + int size = v.size(); + if (size > 0) + return v.toArray(new String[size]); + else // empty list + return null; + } + + public String getKey() { + return key; + } + + public boolean keyEquals(String k) { + if (key != null && key.equalsIgnoreCase(k)) + return true; + else + return false; + } + + public int getNumber() { + return number; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/INTERNALDATE.java b/app/src/main/java/com/sun/mail/imap/protocol/INTERNALDATE.java new file mode 100644 index 0000000000..7f432ff51c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/INTERNALDATE.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.Date; +import java.util.TimeZone; +import java.util.Locale; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.text.FieldPosition; + +import javax.mail.internet.MailDateFormat; + +import com.sun.mail.iap.*; + + +/** + * An INTERNALDATE FETCH item. + * + * @author John Mani + */ + +public class INTERNALDATE implements Item { + + static final char[] name = + {'I','N','T','E','R','N','A','L','D','A','T','E'}; + public int msgno; + protected Date date; + + /* + * Used to parse dates only. The parse method is thread safe + * so we only need to create a single object for use by all + * instances. We depend on the fact that the MailDateFormat + * class will parse dates in INTERNALDATE format as well as + * dates in RFC 822 format. + */ + private static final MailDateFormat mailDateFormat = new MailDateFormat(); + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public INTERNALDATE(FetchResponse r) throws ParsingException { + msgno = r.getNumber(); + r.skipSpaces(); + String s = r.readString(); + if (s == null) + throw new ParsingException("INTERNALDATE is NIL"); + try { + synchronized (mailDateFormat) { + date = mailDateFormat.parse(s); + } + } catch (ParseException pex) { + throw new ParsingException("INTERNALDATE parse error"); + } + } + + public Date getDate() { + return date; + } + + // INTERNALDATE formatter + + private static SimpleDateFormat df = + // Need Locale.US, the "MMM" field can produce unexpected values + // in non US locales ! + new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss ", Locale.US); + + /** + * Format given Date object into INTERNALDATE string + * + * @param d the Date + * @return INTERNALDATE string + */ + public static String format(Date d) { + /* + * SimpleDateFormat objects aren't thread safe, so rather + * than create a separate such object for each request, + * we create one object and synchronize its use here + * so that only one thread is using it at a time. This + * trades off some potential concurrency for speed in the + * common case. + * + * This method is only used when formatting the date in a + * message that's being appended to a folder. + */ + StringBuffer sb = new StringBuffer(); + synchronized (df) { + df.format(d, sb, new FieldPosition(0)); + } + + // compute timezone offset string + TimeZone tz = TimeZone.getDefault(); + int offset = tz.getOffset(d.getTime()); // get offset from GMT + int rawOffsetInMins = offset / 60 / 1000; // offset from GMT in mins + if (rawOffsetInMins < 0) { + sb.append('-'); + rawOffsetInMins = (-rawOffsetInMins); + } else + sb.append('+'); + + int offsetInHrs = rawOffsetInMins / 60; + int offsetInMins = rawOffsetInMins % 60; + + sb.append(Character.forDigit((offsetInHrs/10), 10)); + sb.append(Character.forDigit((offsetInHrs%10), 10)); + sb.append(Character.forDigit((offsetInMins/10), 10)); + sb.append(Character.forDigit((offsetInMins%10), 10)); + + return sb.toString(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/Item.java b/app/src/main/java/com/sun/mail/imap/protocol/Item.java new file mode 100644 index 0000000000..8d604ab939 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/Item.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +/** + * A tagging interface for all IMAP data items. + * Note that the "name" field of all IMAP items MUST be in uppercase.

+ * + * See the BODY, BODYSTRUCTURE, ENVELOPE, FLAGS, INTERNALDATE, RFC822DATA, + * RFC822SIZE, and UID classes. + * + * @author John Mani + */ + +public interface Item { +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/ListInfo.java b/app/src/main/java/com/sun/mail/imap/protocol/ListInfo.java new file mode 100644 index 0000000000..0db1f028e8 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/ListInfo.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; + +import com.sun.mail.iap.*; + +/** + * A LIST response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class ListInfo { + public String name = null; + public char separator = '/'; + public boolean hasInferiors = true; + public boolean canOpen = true; + public int changeState = INDETERMINATE; + public String[] attrs; + + public static final int CHANGED = 1; + public static final int UNCHANGED = 2; + public static final int INDETERMINATE = 3; + + public ListInfo(IMAPResponse r) throws ParsingException { + String[] s = r.readSimpleList(); + + List v = new ArrayList<>(); // accumulate attributes + if (s != null) { + // non-empty attribute list + for (int i = 0; i < s.length; i++) { + if (s[i].equalsIgnoreCase("\\Marked")) + changeState = CHANGED; + else if (s[i].equalsIgnoreCase("\\Unmarked")) + changeState = UNCHANGED; + else if (s[i].equalsIgnoreCase("\\Noselect")) + canOpen = false; + else if (s[i].equalsIgnoreCase("\\Noinferiors")) + hasInferiors = false; + v.add(s[i]); + } + } + attrs = v.toArray(new String[v.size()]); + + r.skipSpaces(); + if (r.readByte() == '"') { + if ((separator = (char)r.readByte()) == '\\') + // escaped separator character + separator = (char)r.readByte(); + r.skip(1); // skip <"> + } else // NIL + r.skip(2); + + r.skipSpaces(); + name = r.readAtomString(); + + if (!r.supportsUtf8()) + // decode the name (using RFC2060's modified UTF7) + name = BASE64MailboxDecoder.decode(name); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/MODSEQ.java b/app/src/main/java/com/sun/mail/imap/protocol/MODSEQ.java new file mode 100644 index 0000000000..f800569309 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/MODSEQ.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.*; + +/** + * This class represents the MODSEQ data item. + * + * @since JavaMail 1.5.1 + * @author Bill Shannon + */ + +public class MODSEQ implements Item { + + static final char[] name = {'M','O','D','S','E','Q'}; + public int seqnum; + + public long modseq; + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public MODSEQ(FetchResponse r) throws ParsingException { + seqnum = r.getNumber(); + r.skipSpaces(); + + if (r.readByte() != '(') + throw new ParsingException("MODSEQ parse error"); + + modseq = r.readLong(); + + if (!r.isNextNonSpace(')')) + throw new ParsingException("MODSEQ parse error"); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/MailboxInfo.java b/app/src/main/java/com/sun/mail/imap/protocol/MailboxInfo.java new file mode 100644 index 0000000000..ccff0b6964 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/MailboxInfo.java @@ -0,0 +1,163 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; + +import javax.mail.Flags; + +import com.sun.mail.iap.*; + +/** + * Information collected when opening a mailbox. + * + * @author John Mani + * @author Bill Shannon + */ + +public class MailboxInfo { + /** The available flags. */ + public Flags availableFlags = null; + /** The permanent flags. */ + public Flags permanentFlags = null; + /** The total number of messages. */ + public int total = -1; + /** The number of recent messages. */ + public int recent = -1; + /** The first unseen message. */ + public int first = -1; + /** The UIDVALIDITY. */ + public long uidvalidity = -1; + /** The next UID value to be assigned. */ + public long uidnext = -1; + /** UIDs are not sticky. */ + public boolean uidNotSticky = false; // RFC 4315 + /** The highest MODSEQ value. */ + public long highestmodseq = -1; // RFC 4551 - CONDSTORE + /** Folder.READ_WRITE or Folder.READ_ONLY, set by IMAPProtocol. */ + public int mode; + /** VANISHED or FETCH responses received while opening the mailbox. */ + public List responses; + + /** + * Collect the information about this mailbox from the + * responses to a SELECT or EXAMINE. + * + * @param r the responses + * @exception ParsingException for errors parsing the responses + */ + public MailboxInfo(Response[] r) throws ParsingException { + for (int i = 0; i < r.length; i++) { + if (r[i] == null || !(r[i] instanceof IMAPResponse)) + continue; + + IMAPResponse ir = (IMAPResponse)r[i]; + + if (ir.keyEquals("EXISTS")) { + total = ir.getNumber(); + r[i] = null; // remove this response + } else if (ir.keyEquals("RECENT")) { + recent = ir.getNumber(); + r[i] = null; // remove this response + } else if (ir.keyEquals("FLAGS")) { + availableFlags = new FLAGS(ir); + r[i] = null; // remove this response + } else if (ir.keyEquals("VANISHED")) { + if (responses == null) + responses = new ArrayList<>(); + responses.add(ir); + r[i] = null; // remove this response + } else if (ir.keyEquals("FETCH")) { + if (responses == null) + responses = new ArrayList<>(); + responses.add(ir); + r[i] = null; // remove this response + } else if (ir.isUnTagged() && ir.isOK()) { + /* + * should be one of: + * * OK [UNSEEN 12] + * * OK [UIDVALIDITY 3857529045] + * * OK [PERMANENTFLAGS (\Deleted)] + * * OK [UIDNEXT 44] + * * OK [HIGHESTMODSEQ 103] + */ + ir.skipSpaces(); + + if (ir.readByte() != '[') { // huh ??? + ir.reset(); + continue; + } + + boolean handled = true; + String s = ir.readAtom(); + if (s.equalsIgnoreCase("UNSEEN")) + first = ir.readNumber(); + else if (s.equalsIgnoreCase("UIDVALIDITY")) + uidvalidity = ir.readLong(); + else if (s.equalsIgnoreCase("PERMANENTFLAGS")) + permanentFlags = new FLAGS(ir); + else if (s.equalsIgnoreCase("UIDNEXT")) + uidnext = ir.readLong(); + else if (s.equalsIgnoreCase("HIGHESTMODSEQ")) + highestmodseq = ir.readLong(); + else + handled = false; // possibly an ALERT + + if (handled) + r[i] = null; // remove this response + else + ir.reset(); // so ALERT can be read + } else if (ir.isUnTagged() && ir.isNO()) { + /* + * should be one of: + * * NO [UIDNOTSTICKY] + */ + ir.skipSpaces(); + + if (ir.readByte() != '[') { // huh ??? + ir.reset(); + continue; + } + + boolean handled = true; + String s = ir.readAtom(); + if (s.equalsIgnoreCase("UIDNOTSTICKY")) + uidNotSticky = true; + else + handled = false; // possibly an ALERT + + if (handled) + r[i] = null; // remove this response + else + ir.reset(); // so ALERT can be read + } + } + + /* + * The PERMANENTFLAGS response code is optional, and if + * not present implies that all flags in the required FLAGS + * response can be changed permanently. + */ + if (permanentFlags == null) { + if (availableFlags != null) + permanentFlags = new Flags(availableFlags); + else + permanentFlags = new Flags(); + } + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/MessageSet.java b/app/src/main/java/com/sun/mail/imap/protocol/MessageSet.java new file mode 100644 index 0000000000..a7e6cc6b5b --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/MessageSet.java @@ -0,0 +1,120 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; + +/** + * This class holds the 'start' and 'end' for a range of messages. + */ +public class MessageSet { + + public int start; + public int end; + + public MessageSet() { } + + public MessageSet(int start, int end) { + this.start = start; + this.end = end; + } + + /** + * Count the total number of elements in a MessageSet + * + * @return how many messages in this MessageSet + */ + public int size() { + return end - start + 1; + } + + /** + * Convert an array of integers into an array of MessageSets + * + * @param msgs the messages + * @return array of MessageSet objects + */ + public static MessageSet[] createMessageSets(int[] msgs) { + List v = new ArrayList<>(); + int i,j; + + for (i=0; i < msgs.length; i++) { + MessageSet ms = new MessageSet(); + ms.start = msgs[i]; + + // Look for contiguous elements + for (j=i+1; j < msgs.length; j++) { + if (msgs[j] != msgs[j-1] +1) + break; + } + ms.end = msgs[j-1]; + v.add(ms); + i = j-1; // i gets incremented @ top of the loop + } + return v.toArray(new MessageSet[v.size()]); + } + + /** + * Convert an array of MessageSets into an IMAP sequence range + * + * @param msgsets the MessageSets + * @return IMAP sequence string + */ + public static String toString(MessageSet[] msgsets) { + if (msgsets == null || msgsets.length == 0) // Empty msgset + return null; + + int i = 0; // msgset index + StringBuilder s = new StringBuilder(); + int size = msgsets.length; + int start, end; + + for (;;) { + start = msgsets[i].start; + end = msgsets[i].end; + + if (end > start) + s.append(start).append(':').append(end); + else // end == start means only one element + s.append(start); + + i++; // Next MessageSet + if (i >= size) // No more MessageSets + break; + else + s.append(','); + } + return s.toString(); + } + + + /* + * Count the total number of elements in an array of MessageSets + */ + public static int size(MessageSet[] msgsets) { + int count = 0; + + if (msgsets == null) // Null msgset + return 0; + + for (int i=0; i < msgsets.length; i++) + count += msgsets[i].size(); + + return count; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/Namespaces.java b/app/src/main/java/com/sun/mail/imap/protocol/Namespaces.java new file mode 100644 index 0000000000..df54e67e2c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/Namespaces.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.*; +import com.sun.mail.iap.*; + +/** + * This class and its inner class represent the response to the + * NAMESPACE command.

+ * + * See RFC 2342. + * + * @author Bill Shannon + */ + +public class Namespaces { + + /** + * A single namespace entry. + */ + public static class Namespace { + /** + * Prefix string for the namespace. + */ + public String prefix; + + /** + * Delimiter between names in this namespace. + */ + public char delimiter; + + /** + * Parse a namespace element out of the response. + * + * @param r the Response to parse + * @exception ProtocolException for any protocol errors + */ + public Namespace(Response r) throws ProtocolException { + // Namespace_Element = "(" string SP (<"> QUOTED_CHAR <"> / nil) + // *(Namespace_Response_Extension) ")" + if (!r.isNextNonSpace('(')) + throw new ProtocolException( + "Missing '(' at start of Namespace"); + // first, the prefix + prefix = r.readString(); + if (!r.supportsUtf8()) + prefix = BASE64MailboxDecoder.decode(prefix); + r.skipSpaces(); + // delimiter is a quoted character or NIL + if (r.peekByte() == '"') { + r.readByte(); + delimiter = (char)r.readByte(); + if (delimiter == '\\') + delimiter = (char)r.readByte(); + if (r.readByte() != '"') + throw new ProtocolException( + "Missing '\"' at end of QUOTED_CHAR"); + } else { + String s = r.readAtom(); + if (s == null) + throw new ProtocolException("Expected NIL, got null"); + if (!s.equalsIgnoreCase("NIL")) + throw new ProtocolException("Expected NIL, got " + s); + delimiter = 0; + } + // at end of Namespace data? + if (r.isNextNonSpace(')')) + return; + + // otherwise, must be a Namespace_Response_Extension + // Namespace_Response_Extension = SP string SP + // "(" string *(SP string) ")" + r.readString(); + r.skipSpaces(); + r.readStringList(); + if (!r.isNextNonSpace(')')) + throw new ProtocolException("Missing ')' at end of Namespace"); + } + }; + + /** + * The personal namespaces. + * May be null. + */ + public Namespace[] personal; + + /** + * The namespaces for other users. + * May be null. + */ + public Namespace[] otherUsers; + + /** + * The shared namespace. + * May be null. + */ + public Namespace[] shared; + + /** + * Parse out all the namespaces. + * + * @param r the Response to parse + * @throws ProtocolException for any protocol errors + */ + public Namespaces(Response r) throws ProtocolException { + personal = getNamespaces(r); + otherUsers = getNamespaces(r); + shared = getNamespaces(r); + } + + /** + * Parse out one of the three sets of namespaces. + */ + private Namespace[] getNamespaces(Response r) throws ProtocolException { + // Namespace = nil / "(" 1*( Namespace_Element) ")" + if (r.isNextNonSpace('(')) { + List v = new ArrayList<>(); + do { + Namespace ns = new Namespace(r); + v.add(ns); + } while (!r.isNextNonSpace(')')); + return v.toArray(new Namespace[v.size()]); + } else { + String s = r.readAtom(); + if (s == null) + throw new ProtocolException("Expected NIL, got null"); + if (!s.equalsIgnoreCase("NIL")) + throw new ProtocolException("Expected NIL, got " + s); + return null; + } + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/RFC822DATA.java b/app/src/main/java/com/sun/mail/imap/protocol/RFC822DATA.java new file mode 100644 index 0000000000..bda754f61d --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/RFC822DATA.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.io.ByteArrayInputStream; +import com.sun.mail.iap.*; +import com.sun.mail.util.ASCIIUtility; + +/** + * The RFC822 response data item. + * + * @author John Mani + * @author Bill Shannon + */ + +public class RFC822DATA implements Item { + + static final char[] name = {'R','F','C','8','2','2'}; + private final int msgno; + private final ByteArray data; + private final boolean isHeader; + + /** + * Constructor, header flag is false. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public RFC822DATA(FetchResponse r) throws ParsingException { + this(r, false); + } + + /** + * Constructor, specifying header flag. + * + * @param r the FetchResponse + * @param isHeader just header information? + * @exception ParsingException for parsing failures + */ + public RFC822DATA(FetchResponse r, boolean isHeader) + throws ParsingException { + this.isHeader = isHeader; + msgno = r.getNumber(); + r.skipSpaces(); + data = r.readByteArray(); + } + + public ByteArray getByteArray() { + return data; + } + + public ByteArrayInputStream getByteArrayInputStream() { + if (data != null) + return data.toByteArrayInputStream(); + else + return null; + } + + public boolean isHeader() { + return isHeader; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/RFC822SIZE.java b/app/src/main/java/com/sun/mail/imap/protocol/RFC822SIZE.java new file mode 100644 index 0000000000..59118f1583 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/RFC822SIZE.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.*; + +/** + * An RFC822SIZE FETCH item. + * + * @author John Mani + */ + +public class RFC822SIZE implements Item { + + static final char[] name = {'R','F','C','8','2','2','.','S','I','Z','E'}; + public int msgno; + + public long size; + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public RFC822SIZE(FetchResponse r) throws ParsingException { + msgno = r.getNumber(); + r.skipSpaces(); + size = r.readLong(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/SaslAuthenticator.java b/app/src/main/java/com/sun/mail/imap/protocol/SaslAuthenticator.java new file mode 100644 index 0000000000..4c31c76715 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/SaslAuthenticator.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.ProtocolException; + +/** + * Interface to make it easier to call IMAPSaslAuthenticator. + */ + +public interface SaslAuthenticator { + public boolean authenticate(String[] mechs, String realm, String authzid, + String u, String p) throws ProtocolException; + +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/SearchSequence.java b/app/src/main/java/com/sun/mail/imap/protocol/SearchSequence.java new file mode 100644 index 0000000000..c1388373ca --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/SearchSequence.java @@ -0,0 +1,527 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.*; +import java.io.IOException; + +import javax.mail.*; +import javax.mail.search.*; +import com.sun.mail.iap.*; +import com.sun.mail.imap.OlderTerm; +import com.sun.mail.imap.YoungerTerm; +import com.sun.mail.imap.ModifiedSinceTerm; + +/** + * This class traverses a search-tree and generates the + * corresponding IMAP search sequence. + * + * Each IMAPProtocol instance contains an instance of this class, + * which might be subclassed by subclasses of IMAPProtocol to add + * support for additional product-specific search terms. + * + * @author John Mani + * @author Bill Shannon + */ +public class SearchSequence { + + private IMAPProtocol protocol; // for hasCapability checks; may be null + + /** + * Create a SearchSequence for this IMAPProtocol. + * + * @param p the IMAPProtocol object for the server + * @since JavaMail 1.6.0 + */ + public SearchSequence(IMAPProtocol p) { + protocol = p; + } + + /** + * Create a SearchSequence. + */ + @Deprecated + public SearchSequence() { + } + + /** + * Generate the IMAP search sequence for the given search expression. + * + * @param term the search term + * @param charset charset for the search + * @return the SEARCH Argument + * @exception SearchException for failures + * @exception IOException for I/O errors + */ + public Argument generateSequence(SearchTerm term, String charset) + throws SearchException, IOException { + /* + * Call the appropriate handler depending on the type of + * the search-term ... + */ + if (term instanceof AndTerm) // AND + return and((AndTerm)term, charset); + else if (term instanceof OrTerm) // OR + return or((OrTerm)term, charset); + else if (term instanceof NotTerm) // NOT + return not((NotTerm)term, charset); + else if (term instanceof HeaderTerm) // HEADER + return header((HeaderTerm)term, charset); + else if (term instanceof FlagTerm) // FLAG + return flag((FlagTerm)term); + else if (term instanceof FromTerm) { // FROM + FromTerm fterm = (FromTerm)term; + return from(fterm.getAddress().toString(), charset); + } + else if (term instanceof FromStringTerm) { // FROM + FromStringTerm fterm = (FromStringTerm)term; + return from(fterm.getPattern(), charset); + } + else if (term instanceof RecipientTerm) { // RECIPIENT + RecipientTerm rterm = (RecipientTerm)term; + return recipient(rterm.getRecipientType(), + rterm.getAddress().toString(), + charset); + } + else if (term instanceof RecipientStringTerm) { // RECIPIENT + RecipientStringTerm rterm = (RecipientStringTerm)term; + return recipient(rterm.getRecipientType(), + rterm.getPattern(), + charset); + } + else if (term instanceof SubjectTerm) // SUBJECT + return subject((SubjectTerm)term, charset); + else if (term instanceof BodyTerm) // BODY + return body((BodyTerm)term, charset); + else if (term instanceof SizeTerm) // SIZE + return size((SizeTerm)term); + else if (term instanceof SentDateTerm) // SENTDATE + return sentdate((SentDateTerm)term); + else if (term instanceof ReceivedDateTerm) // INTERNALDATE + return receiveddate((ReceivedDateTerm)term); + else if (term instanceof OlderTerm) // RFC 5032 OLDER + return older((OlderTerm)term); + else if (term instanceof YoungerTerm) // RFC 5032 YOUNGER + return younger((YoungerTerm)term); + else if (term instanceof MessageIDTerm) // MessageID + return messageid((MessageIDTerm)term, charset); + else if (term instanceof ModifiedSinceTerm) // RFC 4551 MODSEQ + return modifiedSince((ModifiedSinceTerm)term); + else + throw new SearchException("Search too complex"); + } + + /** + * Check if the "text" terms in the given SearchTerm contain + * non US-ASCII characters. + * + * @param term the search term + * @return true if only ASCII + */ + public static boolean isAscii(SearchTerm term) { + if (term instanceof AndTerm) + return isAscii(((AndTerm)term).getTerms()); + else if (term instanceof OrTerm) + return isAscii(((OrTerm)term).getTerms()); + else if (term instanceof NotTerm) + return isAscii(((NotTerm)term).getTerm()); + else if (term instanceof StringTerm) + return isAscii(((StringTerm)term).getPattern()); + else if (term instanceof AddressTerm) + return isAscii(((AddressTerm)term).getAddress().toString()); + + // Any other term returns true. + return true; + } + + /** + * Check if any of the "text" terms in the given SearchTerms contain + * non US-ASCII characters. + * + * @param terms the search terms + * @return true if only ASCII + */ + public static boolean isAscii(SearchTerm[] terms) { + for (int i = 0; i < terms.length; i++) + if (!isAscii(terms[i])) // outta here ! + return false; + return true; + } + + /** + * Does this string contain only ASCII characters? + * + * @param s the string + * @return true if only ASCII + */ + public static boolean isAscii(String s) { + int l = s.length(); + + for (int i=0; i < l; i++) { + if ((int)s.charAt(i) > 0177) // non-ascii + return false; + } + return true; + } + + protected Argument and(AndTerm term, String charset) + throws SearchException, IOException { + // Combine the sequences for both terms + SearchTerm[] terms = term.getTerms(); + // Generate the search sequence for the first term + Argument result = generateSequence(terms[0], charset); + // Append other terms + for (int i = 1; i < terms.length; i++) + result.append(generateSequence(terms[i], charset)); + return result; + } + + protected Argument or(OrTerm term, String charset) + throws SearchException, IOException { + SearchTerm[] terms = term.getTerms(); + + /* The IMAP OR operator takes only two operands. So if + * we have more than 2 operands, group them into 2-operand + * OR Terms. + */ + if (terms.length > 2) { + SearchTerm t = terms[0]; + + // Include rest of the terms + for (int i = 1; i < terms.length; i++) + t = new OrTerm(t, terms[i]); + + term = (OrTerm)t; // set 'term' to the new jumbo OrTerm we + // just created + terms = term.getTerms(); + } + + // 'term' now has only two operands + Argument result = new Argument(); + + // Add the OR search-key, if more than one term + if (terms.length > 1) + result.writeAtom("OR"); + + /* If this term is an AND expression, we need to enclose it + * within paranthesis. + * + * AND expressions are either AndTerms or FlagTerms + */ + if (terms[0] instanceof AndTerm || terms[0] instanceof FlagTerm) + result.writeArgument(generateSequence(terms[0], charset)); + else + result.append(generateSequence(terms[0], charset)); + + // Repeat the above for the second term, if there is one + if (terms.length > 1) { + if (terms[1] instanceof AndTerm || terms[1] instanceof FlagTerm) + result.writeArgument(generateSequence(terms[1], charset)); + else + result.append(generateSequence(terms[1], charset)); + } + + return result; + } + + protected Argument not(NotTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + // Add the NOT search-key + result.writeAtom("NOT"); + + /* If this term is an AND expression, we need to enclose it + * within paranthesis. + * + * AND expressions are either AndTerms or FlagTerms + */ + SearchTerm nterm = term.getTerm(); + if (nterm instanceof AndTerm || nterm instanceof FlagTerm) + result.writeArgument(generateSequence(nterm, charset)); + else + result.append(generateSequence(nterm, charset)); + + return result; + } + + protected Argument header(HeaderTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + result.writeAtom("HEADER"); + result.writeString(term.getHeaderName()); + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument messageid(MessageIDTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + result.writeAtom("HEADER"); + result.writeString("Message-ID"); + // XXX confirm that charset conversion ought to be done + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument flag(FlagTerm term) throws SearchException { + boolean set = term.getTestSet(); + + Argument result = new Argument(); + + Flags flags = term.getFlags(); + Flags.Flag[] sf = flags.getSystemFlags(); + String[] uf = flags.getUserFlags(); + if (sf.length == 0 && uf.length == 0) + throw new SearchException("Invalid FlagTerm"); + + for (int i = 0; i < sf.length; i++) { + if (sf[i] == Flags.Flag.DELETED) + result.writeAtom(set ? "DELETED": "UNDELETED"); + else if (sf[i] == Flags.Flag.ANSWERED) + result.writeAtom(set ? "ANSWERED": "UNANSWERED"); + else if (sf[i] == Flags.Flag.DRAFT) + result.writeAtom(set ? "DRAFT": "UNDRAFT"); + else if (sf[i] == Flags.Flag.FLAGGED) + result.writeAtom(set ? "FLAGGED": "UNFLAGGED"); + else if (sf[i] == Flags.Flag.RECENT) + result.writeAtom(set ? "RECENT": "OLD"); + else if (sf[i] == Flags.Flag.SEEN) + result.writeAtom(set ? "SEEN": "UNSEEN"); + } + + for (int i = 0; i < uf.length; i++) { + result.writeAtom(set ? "KEYWORD" : "UNKEYWORD"); + result.writeAtom(uf[i]); + } + + return result; + } + + protected Argument from(String address, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + result.writeAtom("FROM"); + result.writeString(address, charset); + return result; + } + + protected Argument recipient(Message.RecipientType type, + String address, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + if (type == Message.RecipientType.TO) + result.writeAtom("TO"); + else if (type == Message.RecipientType.CC) + result.writeAtom("CC"); + else if (type == Message.RecipientType.BCC) + result.writeAtom("BCC"); + else + throw new SearchException("Illegal Recipient type"); + + result.writeString(address, charset); + return result; + } + + protected Argument subject(SubjectTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + result.writeAtom("SUBJECT"); + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument body(BodyTerm term, String charset) + throws SearchException, IOException { + Argument result = new Argument(); + + result.writeAtom("BODY"); + result.writeString(term.getPattern(), charset); + return result; + } + + protected Argument size(SizeTerm term) + throws SearchException { + Argument result = new Argument(); + + switch (term.getComparison()) { + case ComparisonTerm.GT: + result.writeAtom("LARGER"); + break; + case ComparisonTerm.LT: + result.writeAtom("SMALLER"); + break; + default: + // GT and LT is all we get from IMAP for size + throw new SearchException("Cannot handle Comparison"); + } + + result.writeNumber(term.getNumber()); + return result; + } + + // Date SEARCH stuff ... + + // NOTE: The built-in IMAP date comparisons are equivalent to + // "<" (BEFORE), "=" (ON), and ">=" (SINCE)!!! + // There is no built-in greater-than comparison! + + /** + * Print an IMAP Date string, that is suitable for the Date + * SEARCH commands. + * + * The IMAP Date string is : + * date ::= date_day "-" date_month "-" date_year + * + * Note that this format does not contain the TimeZone + */ + private static String monthTable[] = { + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" + }; + + // A GregorianCalendar object in the current timezone + protected Calendar cal = new GregorianCalendar(); + + protected String toIMAPDate(Date date) { + StringBuilder s = new StringBuilder(); + + cal.setTime(date); + + s.append(cal.get(Calendar.DATE)).append("-"); + s.append(monthTable[cal.get(Calendar.MONTH)]).append('-'); + s.append(cal.get(Calendar.YEAR)); + + return s.toString(); + } + + protected Argument sentdate(DateTerm term) + throws SearchException { + Argument result = new Argument(); + String date = toIMAPDate(term.getDate()); + + switch (term.getComparison()) { + case ComparisonTerm.GT: + result.writeAtom("NOT SENTON " + date + " SENTSINCE " + date); + break; + case ComparisonTerm.EQ: + result.writeAtom("SENTON " + date); + break; + case ComparisonTerm.LT: + result.writeAtom("SENTBEFORE " + date); + break; + case ComparisonTerm.GE: + result.writeAtom("SENTSINCE " + date); + break; + case ComparisonTerm.LE: + result.writeAtom("OR SENTBEFORE " + date + " SENTON " + date); + break; + case ComparisonTerm.NE: + result.writeAtom("NOT SENTON " + date); + break; + default: + throw new SearchException("Cannot handle Date Comparison"); + } + + return result; + } + + protected Argument receiveddate(DateTerm term) + throws SearchException { + Argument result = new Argument(); + String date = toIMAPDate(term.getDate()); + + switch (term.getComparison()) { + case ComparisonTerm.GT: + result.writeAtom("NOT ON " + date + " SINCE " + date); + break; + case ComparisonTerm.EQ: + result.writeAtom("ON " + date); + break; + case ComparisonTerm.LT: + result.writeAtom("BEFORE " + date); + break; + case ComparisonTerm.GE: + result.writeAtom("SINCE " + date); + break; + case ComparisonTerm.LE: + result.writeAtom("OR BEFORE " + date + " ON " + date); + break; + case ComparisonTerm.NE: + result.writeAtom("NOT ON " + date); + break; + default: + throw new SearchException("Cannot handle Date Comparison"); + } + + return result; + } + + /** + * Generate argument for OlderTerm. + * + * @param term the search term + * @return the SEARCH Argument + * @exception SearchException for failures + * @since JavaMail 1.5.1 + */ + protected Argument older(OlderTerm term) throws SearchException { + if (protocol != null && !protocol.hasCapability("WITHIN")) + throw new SearchException("Server doesn't support OLDER searches"); + Argument result = new Argument(); + result.writeAtom("OLDER"); + result.writeNumber(term.getInterval()); + return result; + } + + /** + * Generate argument for YoungerTerm. + * + * @param term the search term + * @return the SEARCH Argument + * @exception SearchException for failures + * @since JavaMail 1.5.1 + */ + protected Argument younger(YoungerTerm term) throws SearchException { + if (protocol != null && !protocol.hasCapability("WITHIN")) + throw new SearchException("Server doesn't support YOUNGER searches"); + Argument result = new Argument(); + result.writeAtom("YOUNGER"); + result.writeNumber(term.getInterval()); + return result; + } + + /** + * Generate argument for ModifiedSinceTerm. + * + * @param term the search term + * @return the SEARCH Argument + * @exception SearchException for failures + * @since JavaMail 1.5.1 + */ + protected Argument modifiedSince(ModifiedSinceTerm term) + throws SearchException { + if (protocol != null && !protocol.hasCapability("CONDSTORE")) + throw new SearchException("Server doesn't support MODSEQ searches"); + Argument result = new Argument(); + result.writeAtom("MODSEQ"); + result.writeNumber(term.getModSeq()); + return result; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/Status.java b/app/src/main/java/com/sun/mail/imap/protocol/Status.java new file mode 100644 index 0000000000..76d29c358b --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/Status.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.Map; +import java.util.HashMap; +import java.util.Locale; + +import com.sun.mail.iap.*; + +/** + * STATUS response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class Status { + public String mbox = null; + public int total = -1; + public int recent = -1; + public long uidnext = -1; + public long uidvalidity = -1; + public int unseen = -1; + public long highestmodseq = -1; + public Map items; // any unknown items + + static final String[] standardItems = + { "MESSAGES", "RECENT", "UNSEEN", "UIDNEXT", "UIDVALIDITY" }; + + public Status(Response r) throws ParsingException { + // mailbox := astring + mbox = r.readAtomString(); + if (!r.supportsUtf8()) + mbox = BASE64MailboxDecoder.decode(mbox); + + // Workaround buggy IMAP servers that don't quote folder names + // with spaces. + final StringBuilder buffer = new StringBuilder(); + boolean onlySpaces = true; + + while (r.peekByte() != '(' && r.peekByte() != 0) { + final char next = (char)r.readByte(); + + buffer.append(next); + + if (next != ' ') { + onlySpaces = false; + } + } + + if (!onlySpaces) { + mbox = (mbox + buffer).trim(); + } + + if (r.readByte() != '(') + throw new ParsingException("parse error in STATUS"); + + do { + String attr = r.readAtom(); + if (attr == null) + throw new ParsingException("parse error in STATUS"); + if (attr.equalsIgnoreCase("MESSAGES")) + total = r.readNumber(); + else if (attr.equalsIgnoreCase("RECENT")) + recent = r.readNumber(); + else if (attr.equalsIgnoreCase("UIDNEXT")) + uidnext = r.readLong(); + else if (attr.equalsIgnoreCase("UIDVALIDITY")) + uidvalidity = r.readLong(); + else if (attr.equalsIgnoreCase("UNSEEN")) + unseen = r.readNumber(); + else if (attr.equalsIgnoreCase("HIGHESTMODSEQ")) + highestmodseq = r.readLong(); + else { + if (items == null) + items = new HashMap<>(); + items.put(attr.toUpperCase(Locale.ENGLISH), + Long.valueOf(r.readLong())); + } + } while (!r.isNextNonSpace(')')); + } + + /** + * Get the value for the STATUS item. + * + * @param item the STATUS item + * @return the value + * @since JavaMail 1.5.2 + */ + public long getItem(String item) { + item = item.toUpperCase(Locale.ENGLISH); + Long v; + long ret = -1; + if (items != null && (v = items.get(item)) != null) + ret = v.longValue(); + else if (item.equals("MESSAGES")) + ret = total; + else if (item.equals("RECENT")) + ret = recent; + else if (item.equals("UIDNEXT")) + ret = uidnext; + else if (item.equals("UIDVALIDITY")) + ret = uidvalidity; + else if (item.equals("UNSEEN")) + ret = unseen; + else if (item.equals("HIGHESTMODSEQ")) + ret = highestmodseq; + return ret; + } + + public static void add(Status s1, Status s2) { + if (s2.total != -1) + s1.total = s2.total; + if (s2.recent != -1) + s1.recent = s2.recent; + if (s2.uidnext != -1) + s1.uidnext = s2.uidnext; + if (s2.uidvalidity != -1) + s1.uidvalidity = s2.uidvalidity; + if (s2.unseen != -1) + s1.unseen = s2.unseen; + if (s2.highestmodseq != -1) + s1.highestmodseq = s2.highestmodseq; + if (s1.items == null) + s1.items = s2.items; + else if (s2.items != null) + s1.items.putAll(s2.items); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/UID.java b/app/src/main/java/com/sun/mail/imap/protocol/UID.java new file mode 100644 index 0000000000..58082c2104 --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/UID.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import com.sun.mail.iap.*; + +/** + * This class represents the UID data item. + * + * @author John Mani + */ + +public class UID implements Item { + + static final char[] name = {'U','I','D'}; + public int seqnum; + + public long uid; + + /** + * Constructor. + * + * @param r the FetchResponse + * @exception ParsingException for parsing failures + */ + public UID(FetchResponse r) throws ParsingException { + seqnum = r.getNumber(); + r.skipSpaces(); + uid = r.readLong(); + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/UIDSet.java b/app/src/main/java/com/sun/mail/imap/protocol/UIDSet.java new file mode 100644 index 0000000000..222858155c --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/UIDSet.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.imap.protocol; + +import java.util.List; +import java.util.ArrayList; +import java.util.StringTokenizer; + +/** + * This class holds the 'start' and 'end' for a range of UIDs. + * Just like MessageSet except using long instead of int. + */ +public class UIDSet { + + public long start; + public long end; + + public UIDSet() { } + + public UIDSet(long start, long end) { + this.start = start; + this.end = end; + } + + /** + * Count the total number of elements in a UIDSet + * + * @return the number of elements + */ + public long size() { + return end - start + 1; + } + + /** + * Convert an array of longs into an array of UIDSets + * + * @param uids the UIDs + * @return array of UIDSet objects + */ + public static UIDSet[] createUIDSets(long[] uids) { + if (uids == null) + return null; + List v = new ArrayList<>(); + int i,j; + + for (i=0; i < uids.length; i++) { + UIDSet ms = new UIDSet(); + ms.start = uids[i]; + + // Look for contiguous elements + for (j=i+1; j < uids.length; j++) { + if (uids[j] != uids[j-1] +1) + break; + } + ms.end = uids[j-1]; + v.add(ms); + i = j-1; // i gets incremented @ top of the loop + } + UIDSet[] uidset = new UIDSet[v.size()]; + return v.toArray(uidset); + } + + /** + * Parse a string in IMAP UID range format. + * + * @param uids UID string + * @return array of UIDSet objects + * @since JavaMail 1.5.1 + */ + public static UIDSet[] parseUIDSets(String uids) { + if (uids == null) + return null; + List v = new ArrayList<>(); + StringTokenizer st = new StringTokenizer(uids, ",:", true); + long start = -1; + UIDSet cur = null; + try { + while(st.hasMoreTokens()) { + String s = st.nextToken(); + if (s.equals(",")) { + if (cur != null) + v.add(cur); + cur = null; + } else if (s.equals(":")) { + // nothing to do, wait for next number + } else { // better be a number + long n = Long.parseLong(s); + if (cur != null) + cur.end = n; + else + cur = new UIDSet(n, n); + } + } + } catch (NumberFormatException nex) { + // give up and return what we have so far + } + if (cur != null) + v.add(cur); + UIDSet[] uidset = new UIDSet[v.size()]; + return v.toArray(uidset); + } + + /** + * Convert an array of UIDSets into an IMAP sequence range. + * + * @param uidset the UIDSets + * @return the IMAP sequence string + */ + public static String toString(UIDSet[] uidset) { + if (uidset == null) + return null; + if (uidset.length == 0) // Empty uidset + return ""; + + int i = 0; // uidset index + StringBuilder s = new StringBuilder(); + int size = uidset.length; + long start, end; + + for (;;) { + start = uidset[i].start; + end = uidset[i].end; + + if (end > start) + s.append(start).append(':').append(end); + else // end == start means only one element + s.append(start); + + i++; // Next UIDSet + if (i >= size) // No more UIDSets + break; + else + s.append(','); + } + return s.toString(); + } + + /** + * Convert an array of UIDSets into a array of long UIDs. + * + * @param uidset the UIDSets + * @return arrray of UIDs + * @since JavaMail 1.5.1 + */ + public static long[] toArray(UIDSet[] uidset) { + //return toArray(uidset, -1); + if (uidset == null) + return null; + long[] uids = new long[(int)UIDSet.size(uidset)]; + int i = 0; + for (UIDSet u : uidset) { + for (long n = u.start; n <= u.end; n++) + uids[i++] = n; + } + return uids; + } + + /** + * Convert an array of UIDSets into a array of long UIDs. + * Don't include any UIDs larger than uidmax. + * + * @param uidset the UIDSets + * @param uidmax maximum UID + * @return arrray of UIDs + * @since JavaMail 1.5.1 + */ + public static long[] toArray(UIDSet[] uidset, long uidmax) { + if (uidset == null) + return null; + long[] uids = new long[(int)UIDSet.size(uidset, uidmax)]; + int i = 0; + for (UIDSet u : uidset) { + for (long n = u.start; n <= u.end; n++) { + if (uidmax >= 0 && n > uidmax) + break; + uids[i++] = n; + } + } + return uids; + } + + /** + * Count the total number of elements in an array of UIDSets. + * + * @param uidset the UIDSets + * @return the number of elements + */ + public static long size(UIDSet[] uidset) { + long count = 0; + + if (uidset != null) + for (UIDSet u : uidset) + count += u.size(); + + return count; + } + + /** + * Count the total number of elements in an array of UIDSets. + * Don't count UIDs greater then uidmax. + * + * @since JavaMail 1.5.1 + */ + private static long size(UIDSet[] uidset, long uidmax) { + long count = 0; + + if (uidset != null) + for (UIDSet u : uidset) { + if (uidmax < 0) + count += u.size(); + else if (u.start <= uidmax) { + if (u.end < uidmax) + count += u.end - u.start + 1; + else + count += uidmax - u.start + 1; + } + } + + return count; + } +} diff --git a/app/src/main/java/com/sun/mail/imap/protocol/package.html b/app/src/main/java/com/sun/mail/imap/protocol/package.html new file mode 100644 index 0000000000..370726e0fa --- /dev/null +++ b/app/src/main/java/com/sun/mail/imap/protocol/package.html @@ -0,0 +1,33 @@ + + + + + + +com.sun.mail.imap.protocol package + + + +

+This package includes internal IMAP support classes and +SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. +

+ + + diff --git a/app/src/main/java/com/sun/mail/pop3/AppendStream.java b/app/src/main/java/com/sun/mail/pop3/AppendStream.java new file mode 100644 index 0000000000..cc56ce3569 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/AppendStream.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +/** + * A stream for writing to the temp file, and when done can return a stream for + * reading the data just written. NOTE: We assume that only one thread is + * writing to the file at a time. + */ +class AppendStream extends OutputStream { + + private final WritableSharedFile tf; + private RandomAccessFile raf; + private final long start; + private long end; + + public AppendStream(WritableSharedFile tf) throws IOException { + this.tf = tf; + raf = tf.getWritableFile(); + start = raf.length(); + raf.seek(start); + } + + @Override + public void write(int b) throws IOException { + raf.write(b); + } + + @Override + public void write(byte[] b) throws IOException { + raf.write(b); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + raf.write(b, off, len); + } + + @Override + public synchronized void close() throws IOException { + end = tf.updateLength(); + raf = null; // no more writing allowed + } + + public synchronized InputStream getInputStream() throws IOException { + return tf.newStream(start, end); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/DefaultFolder.java b/app/src/main/java/com/sun/mail/pop3/DefaultFolder.java new file mode 100644 index 0000000000..eaf9d6391c --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/DefaultFolder.java @@ -0,0 +1,141 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.*; + +/** + * The POP3 DefaultFolder. Only contains the "INBOX" folder. + * + * @author Christopher Cotton + */ +public class DefaultFolder extends Folder { + + DefaultFolder(POP3Store store) { + super(store); + } + + @Override + public String getName() { + return ""; + } + + @Override + public String getFullName() { + return ""; + } + + @Override + public Folder getParent() { + return null; + } + + @Override + public boolean exists() { + return true; + } + + @Override + public Folder[] list(String pattern) throws MessagingException { + Folder[] f = { getInbox() }; + return f; + } + + @Override + public char getSeparator() { + return '/'; + } + + @Override + public int getType() { + return HOLDS_FOLDERS; + } + + @Override + public boolean create(int type) throws MessagingException { + return false; + } + + @Override + public boolean hasNewMessages() throws MessagingException { + return false; + } + + @Override + public Folder getFolder(String name) throws MessagingException { + if (!name.equalsIgnoreCase("INBOX")) { + throw new MessagingException("only INBOX supported"); + } else { + return getInbox(); + } + } + + protected Folder getInbox() throws MessagingException { + return getStore().getFolder("INBOX"); + } + + + @Override + public boolean delete(boolean recurse) throws MessagingException { + throw new MethodNotSupportedException("delete"); + } + + @Override + public boolean renameTo(Folder f) throws MessagingException { + throw new MethodNotSupportedException("renameTo"); + } + + @Override + public void open(int mode) throws MessagingException { + throw new MethodNotSupportedException("open"); + } + + @Override + public void close(boolean expunge) throws MessagingException { + throw new MethodNotSupportedException("close"); + } + + @Override + public boolean isOpen() { + return false; + } + + @Override + public Flags getPermanentFlags() { + return new Flags(); // empty flags object + } + + @Override + public int getMessageCount() throws MessagingException { + return 0; + } + + @Override + public Message getMessage(int msgno) throws MessagingException { + throw new MethodNotSupportedException("getMessage"); + } + + @Override + public void appendMessages(Message[] msgs) throws MessagingException { + throw new MethodNotSupportedException("Append not supported"); + } + + @Override + public Message[] expunge() throws MessagingException { + throw new MethodNotSupportedException("expunge"); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3Folder.java b/app/src/main/java/com/sun/mail/pop3/POP3Folder.java new file mode 100644 index 0000000000..a045a24ae2 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3Folder.java @@ -0,0 +1,611 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.*; +import javax.mail.event.*; +import java.io.InputStream; +import java.io.IOException; +import java.io.EOFException; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.lang.reflect.Constructor; + +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.MailLogger; +import java.util.ArrayList; +import java.util.List; + +/** + * A POP3 Folder (can only be "INBOX"). + * + * See the com.sun.mail.pop3 package + * documentation for further information on the POP3 protocol provider.

+ * + * @author Bill Shannon + * @author John Mani (ported to the javax.mail APIs) + */ +public class POP3Folder extends Folder { + + private String name; + private POP3Store store; + private volatile Protocol port; + private int total; + private int size; + private boolean exists = false; + private volatile boolean opened = false; + private POP3Message[] message_cache; + private boolean doneUidl = false; + private volatile TempFile fileCache = null; + private boolean forceClose; + + MailLogger logger; // package private, for POP3Message + + protected POP3Folder(POP3Store store, String name) { + super(store); + this.name = name; + this.store = store; + if (name.equalsIgnoreCase("INBOX")) + exists = true; + logger = new MailLogger(this.getClass(), "DEBUG POP3", + store.getSession().getDebug(), store.getSession().getDebugOut()); + } + + @Override + public String getName() { + return name; + } + + @Override + public String getFullName() { + return name; + } + + @Override + public Folder getParent() { + return new DefaultFolder(store); + } + + /** + * Always true for the folder "INBOX", always false for + * any other name. + * + * @return true for INBOX, false otherwise + */ + @Override + public boolean exists() { + return exists; + } + + /** + * Always throws MessagingException because no POP3 folders + * can contain subfolders. + * + * @exception MessagingException always + */ + @Override + public Folder[] list(String pattern) throws MessagingException { + throw new MessagingException("not a directory"); + } + + /** + * Always returns a NUL character because POP3 doesn't support a hierarchy. + * + * @return NUL + */ + @Override + public char getSeparator() { + return '\0'; + } + + /** + * Always returns Folder.HOLDS_MESSAGES. + * + * @return Folder.HOLDS_MESSAGES + */ + @Override + public int getType() { + return HOLDS_MESSAGES; + } + + /** + * Always returns false; the POP3 protocol doesn't + * support creating folders. + * + * @return false + */ + @Override + public boolean create(int type) throws MessagingException { + return false; + } + + /** + * Always returns false; the POP3 protocol provides + * no way to determine when a new message arrives. + * + * @return false + */ + @Override + public boolean hasNewMessages() throws MessagingException { + return false; // no way to know + } + + /** + * Always throws MessagingException because no POP3 folders + * can contain subfolders. + * + * @exception MessagingException always + */ + @Override + public Folder getFolder(String name) throws MessagingException { + throw new MessagingException("not a directory"); + } + + /** + * Always throws MethodNotSupportedException + * because the POP3 protocol doesn't allow the INBOX to + * be deleted. + * + * @exception MethodNotSupportedException always + */ + @Override + public boolean delete(boolean recurse) throws MessagingException { + throw new MethodNotSupportedException("delete"); + } + + /** + * Always throws MethodNotSupportedException + * because the POP3 protocol doesn't support multiple folders. + * + * @exception MethodNotSupportedException always + */ + @Override + public boolean renameTo(Folder f) throws MessagingException { + throw new MethodNotSupportedException("renameTo"); + } + + /** + * Throws FolderNotFoundException unless this + * folder is named "INBOX". + * + * @exception FolderNotFoundException if not INBOX + * @exception AuthenticationFailedException authentication failures + * @exception MessagingException other open failures + */ + @Override + public synchronized void open(int mode) throws MessagingException { + checkClosed(); + if (!exists) + throw new FolderNotFoundException(this, "folder is not INBOX"); + + try { + port = store.getPort(this); + Status s = port.stat(); + total = s.total; + size = s.size; + this.mode = mode; + if (store.useFileCache) { + try { + fileCache = new TempFile(store.fileCacheDir); + } catch (IOException ex) { + logger.log(Level.FINE, "failed to create file cache", ex); + throw ex; // caught below + } + } + opened = true; + } catch (IOException ioex) { + try { + if (port != null) + port.quit(); + } catch (IOException ioex2) { + // ignore + } finally { + port = null; + store.closePort(this); + } + throw new MessagingException("Open failed", ioex); + } + + // Create the message cache array of appropriate size + message_cache = new POP3Message[total]; + doneUidl = false; + + notifyConnectionListeners(ConnectionEvent.OPENED); + } + + @Override + public synchronized void close(boolean expunge) throws MessagingException { + checkOpen(); + + try { + /* + * Some POP3 servers will mark messages for deletion when + * they're read. To prevent such messages from being + * deleted before the client deletes them, you can set + * the mail.pop3.rsetbeforequit property to true. This + * causes us to issue a POP3 RSET command to clear all + * the "marked for deletion" flags. We can then explicitly + * delete messages as desired. + */ + if (store.rsetBeforeQuit && !forceClose) + port.rset(); + POP3Message m; + if (expunge && mode == READ_WRITE && !forceClose) { + // find all messages marked deleted and issue DELE commands + for (int i = 0; i < message_cache.length; i++) { + if ((m = message_cache[i]) != null) { + if (m.isSet(Flags.Flag.DELETED)) + try { + port.dele(i + 1); + } catch (IOException ioex) { + throw new MessagingException( + "Exception deleting messages during close", + ioex); + } + } + } + } + + /* + * Flush and free all cached data for the messages. + */ + for (int i = 0; i < message_cache.length; i++) { + if ((m = message_cache[i]) != null) + m.invalidate(true); + } + + if (forceClose) + port.close(); + else + port.quit(); + } catch (IOException ex) { + // do nothing + } finally { + port = null; + store.closePort(this); + message_cache = null; + opened = false; + notifyConnectionListeners(ConnectionEvent.CLOSED); + if (fileCache != null) { + fileCache.close(); + fileCache = null; + } + } + } + + @Override + public synchronized boolean isOpen() { + if (!opened) + return false; + try { + if (!port.noop()) + throw new IOException("NOOP failed"); + } catch (IOException ioex) { + try { + close(false); + } catch (MessagingException mex) { + // ignore it + } + return false; + } + return true; + } + + /** + * Always returns an empty Flags object because + * the POP3 protocol doesn't support any permanent flags. + * + * @return empty Flags object + */ + @Override + public Flags getPermanentFlags() { + return new Flags(); // empty flags object + } + + /** + * Will not change while the folder is open because the POP3 + * protocol doesn't support notification of new messages + * arriving in open folders. + */ + @Override + public synchronized int getMessageCount() throws MessagingException { + if (!opened) + return -1; + checkReadable(); + return total; + } + + @Override + public synchronized Message getMessage(int msgno) + throws MessagingException { + checkOpen(); + + POP3Message m; + + // Assuming that msgno is <= total + if ((m = message_cache[msgno-1]) == null) { + m = createMessage(this, msgno); + message_cache[msgno-1] = m; + } + return m; + } + + protected POP3Message createMessage(Folder f, int msgno) + throws MessagingException { + POP3Message m = null; + Constructor cons = store.messageConstructor; + if (cons != null) { + try { + Object[] o = { this, Integer.valueOf(msgno) }; + m = (POP3Message)cons.newInstance(o); + } catch (Exception ex) { + // ignore + } + } + if (m == null) + m = new POP3Message(this, msgno); + return m; + } + + /** + * Always throws MethodNotSupportedException + * because the POP3 protocol doesn't support appending messages. + * + * @exception MethodNotSupportedException always + */ + @Override + public void appendMessages(Message[] msgs) throws MessagingException { + throw new MethodNotSupportedException("Append not supported"); + } + + /** + * Always throws MethodNotSupportedException + * because the POP3 protocol doesn't support expunging messages + * without closing the folder; call the {@link #close close} method + * with the expunge argument set to true + * instead. + * + * @exception MethodNotSupportedException always + */ + @Override + public Message[] expunge() throws MessagingException { + throw new MethodNotSupportedException("Expunge not supported"); + } + + /** + * Prefetch information about POP3 messages. + * If the FetchProfile contains UIDFolder.FetchProfileItem.UID, + * POP3 UIDs for all messages in the folder are fetched using the POP3 + * UIDL command. + * If the FetchProfile contains FetchProfile.Item.ENVELOPE, + * the headers and size of all messages are fetched using the POP3 TOP + * and LIST commands. + */ + @Override + public synchronized void fetch(Message[] msgs, FetchProfile fp) + throws MessagingException { + checkReadable(); + if (!doneUidl && store.supportsUidl && + fp.contains(UIDFolder.FetchProfileItem.UID)) { + /* + * Since the POP3 protocol only lets us fetch the UID + * for a single message or for all messages, we go ahead + * and fetch UIDs for all messages here, ignoring the msgs + * parameter. We could be more intelligent and base this + * decision on the number of messages fetched, or the + * percentage of the total number of messages fetched. + */ + String[] uids = new String[message_cache.length]; + try { + if (!port.uidl(uids)) + return; + } catch (EOFException eex) { + close(false); + throw new FolderClosedException(this, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting UIDL", ex); + } + for (int i = 0; i < uids.length; i++) { + if (uids[i] == null) + continue; + POP3Message m = (POP3Message)getMessage(i + 1); + m.uid = uids[i]; + } + doneUidl = true; // only do this once + } + if (fp.contains(FetchProfile.Item.ENVELOPE)) { + for (int i = 0; i < msgs.length; i++) { + try { + POP3Message msg = (POP3Message)msgs[i]; + // fetch headers + msg.getHeader(""); + // fetch message size + msg.getSize(); + } catch (MessageRemovedException mex) { + // should never happen, but ignore it if it does + } + } + } + } + + /** + * Return the unique ID string for this message, or null if + * not available. Uses the POP3 UIDL command. + * + * @param msg the message + * @return unique ID string + * @exception MessagingException for failures + */ + public synchronized String getUID(Message msg) throws MessagingException { + checkOpen(); + if (!(msg instanceof POP3Message)) + throw new MessagingException("message is not a POP3Message"); + POP3Message m = (POP3Message)msg; + try { + if (!store.supportsUidl) + return null; + if (m.uid == POP3Message.UNKNOWN) + m.uid = port.uidl(m.getMessageNumber()); + return m.uid; + } catch (EOFException eex) { + close(false); + throw new FolderClosedException(this, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting UIDL", ex); + } + } + + /** + * Return the size of this folder, as was returned by the POP3 STAT + * command when this folder was opened. + * + * @return folder size + * @exception IllegalStateException if the folder isn't open + * @exception MessagingException for other failures + */ + public synchronized int getSize() throws MessagingException { + checkOpen(); + return size; + } + + /** + * Return the sizes of all messages in this folder, as returned + * by the POP3 LIST command. Each entry in the array corresponds + * to a message; entry i corresponds to message number i+1. + * + * @return array of message sizes + * @exception IllegalStateException if the folder isn't open + * @exception MessagingException for other failures + * @since JavaMail 1.3.3 + */ + public synchronized int[] getSizes() throws MessagingException { + checkOpen(); + int sizes[] = new int[total]; + InputStream is = null; + LineInputStream lis = null; + try { + is = port.list(); + lis = new LineInputStream(is); + String line; + while ((line = lis.readLine()) != null) { + try { + StringTokenizer st = new StringTokenizer(line); + int msgnum = Integer.parseInt(st.nextToken()); + int size = Integer.parseInt(st.nextToken()); + if (msgnum > 0 && msgnum <= total) + sizes[msgnum - 1] = size; + } catch (RuntimeException e) { + } + } + } catch (IOException ex) { + // ignore it? + } finally { + try { + if (lis != null) + lis.close(); + } catch (IOException cex) { } + try { + if (is != null) + is.close(); + } catch (IOException cex) { } + } + return sizes; + } + + /** + * Return the raw results of the POP3 LIST command with no arguments. + * + * @return InputStream containing results + * @exception IllegalStateException if the folder isn't open + * @exception IOException for I/O errors talking to the server + * @exception MessagingException for other errors + * @since JavaMail 1.3.3 + */ + public synchronized InputStream listCommand() + throws MessagingException, IOException { + checkOpen(); + return port.list(); + } + + /** + * Close the folder when we're finalized. + */ + @Override + protected void finalize() throws Throwable { + forceClose = !store.finalizeCleanClose; + try { + if (opened) + close(false); + } finally { + super.finalize(); + forceClose = false; + } + } + + /* Ensure the folder is open */ + private void checkOpen() throws IllegalStateException { + if (!opened) + throw new IllegalStateException("Folder is not Open"); + } + + /* Ensure the folder is not open */ + private void checkClosed() throws IllegalStateException { + if (opened) + throw new IllegalStateException("Folder is Open"); + } + + /* Ensure the folder is open & readable */ + private void checkReadable() throws IllegalStateException { + if (!opened || (mode != READ_ONLY && mode != READ_WRITE)) + throw new IllegalStateException("Folder is not Readable"); + } + + /* Ensure the folder is open & writable */ + /* + private void checkWritable() throws IllegalStateException { + if (!opened || mode != READ_WRITE) + throw new IllegalStateException("Folder is not Writable"); + } + */ + + /** + * Centralize access to the Protocol object by POP3Message + * objects so that they will fail appropriately when the folder + * is closed. + */ + Protocol getProtocol() throws MessagingException { + Protocol p = port; // read it before close() can set it to null + checkOpen(); + // close() might happen here + return p; + } + + /* + * Only here to make accessible to POP3Message. + */ + @Override + protected void notifyMessageChangedListeners(int type, Message m) { + super.notifyMessageChangedListeners(type, m); + } + + /** + * Used by POP3Message. + */ + TempFile getFileCache() { + return fileCache; + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3Message.java b/app/src/main/java/com/sun/mail/pop3/POP3Message.java new file mode 100644 index 0000000000..7bc9c9fabe --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3Message.java @@ -0,0 +1,644 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.*; +import java.util.Enumeration; +import java.util.logging.Level; +import java.lang.ref.SoftReference; +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.event.*; +import com.sun.mail.util.ReadableMime; + +/** + * A POP3 Message. Just like a MimeMessage except that + * some things are not supported. + * + * @author Bill Shannon + */ +public class POP3Message extends MimeMessage implements ReadableMime { + + /* + * Our locking strategy is to always lock the POP3Folder before the + * POP3Message so we have to be careful to drop our lock before calling + * back to the folder to close it and notify of connection lost events. + */ + + // flag to indicate we haven't tried to fetch the UID yet + static final String UNKNOWN = "UNKNOWN"; + + private POP3Folder folder; // overrides folder in MimeMessage + private int hdrSize = -1; + private int msgSize = -1; + String uid = UNKNOWN; // controlled by folder lock + + // rawData itself is never null + private SoftReference rawData + = new SoftReference<>(null); + + public POP3Message(Folder folder, int msgno) + throws MessagingException { + super(folder, msgno); + assert folder instanceof POP3Folder; + this.folder = (POP3Folder)folder; + } + + /** + * Set the specified flags on this message to the specified value. + * + * @param newFlags the flags to be set + * @param set the value to be set + */ + @Override + public synchronized void setFlags(Flags newFlags, boolean set) + throws MessagingException { + Flags oldFlags = (Flags)flags.clone(); + super.setFlags(newFlags, set); + if (!flags.equals(oldFlags)) + folder.notifyMessageChangedListeners( + MessageChangedEvent.FLAGS_CHANGED, this); + } + + /** + * Return the size of the content of this message in bytes. + * Returns -1 if the size cannot be determined.

+ * + * Note that this number may not be an exact measure of the + * content size and may or may not account for any transfer + * encoding of the content.

+ * + * @return size of content in bytes + * @exception MessagingException for failures + */ + @Override + public int getSize() throws MessagingException { + try { + synchronized (this) { + // if we already have the size, return it + if (msgSize > 0) + return msgSize; + } + + /* + * Use LIST to determine the entire message + * size and subtract out the header size + * (which may involve loading the headers, + * which may load the content as a side effect). + * If the content is loaded as a side effect of + * loading the headers, it will set the size. + * + * Make sure to call loadHeaders() outside of the + * synchronization block. There's a potential race + * condition here but synchronization will occur in + * loadHeaders() to make sure the headers are only + * loaded once, and again in the following block to + * only compute msgSize once. + */ + if (headers == null) + loadHeaders(); + + synchronized (this) { + if (msgSize < 0) + msgSize = folder.getProtocol().list(msgnum) - hdrSize; + return msgSize; + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting size", ex); + } + } + + /** + * Produce the raw bytes of the message. The data is fetched using + * the POP3 RETR command. If skipHeader is true, just the content + * is returned. + */ + private InputStream getRawStream(boolean skipHeader) + throws MessagingException { + InputStream rawcontent = null; + try { + synchronized(this) { + rawcontent = rawData.get(); + if (rawcontent == null) { + TempFile cache = folder.getFileCache(); + if (cache != null) { + if (folder.logger.isLoggable(Level.FINE)) + folder.logger.fine("caching message #" + msgnum + + " in temp file"); + AppendStream os = cache.getAppendStream(); + BufferedOutputStream bos = new BufferedOutputStream(os); + try { + folder.getProtocol().retr(msgnum, bos); + } finally { + bos.close(); + } + rawcontent = os.getInputStream(); + } else { + rawcontent = folder.getProtocol().retr(msgnum, + msgSize > 0 ? msgSize + hdrSize : 0); + } + if (rawcontent == null) { + expunged = true; + throw new MessageRemovedException( + "can't retrieve message #" + msgnum + + " in POP3Message.getContentStream"); // XXX - what else? + } + + if (headers == null || + ((POP3Store)(folder.getStore())).forgetTopHeaders) { + headers = new InternetHeaders(rawcontent); + hdrSize = + (int)((SharedInputStream)rawcontent).getPosition(); + } else { + /* + * Already have the headers, have to skip the headers + * in the content array and return the body. + * + * XXX - It seems that some mail servers return slightly + * different headers in the RETR results than were returned + * in the TOP results, so we can't depend on remembering + * the size of the headers from the TOP command and just + * skipping that many bytes. Instead, we have to process + * the content, skipping over the header until we come to + * the empty line that separates the header from the body. + */ + int offset = 0; + for (;;) { + int len = 0; // number of bytes in this line + int c1; + while ((c1 = rawcontent.read()) >= 0) { + if (c1 == '\n') // end of line + break; + else if (c1 == '\r') { + // got CR, is the next char LF? + if (rawcontent.available() > 0) { + rawcontent.mark(1); + if (rawcontent.read() != '\n') + rawcontent.reset(); + } + break; // in any case, end of line + } + + // not CR, NL, or CRLF, count the byte + len++; + } + // here when end of line or out of data + + // if out of data, we're done + if (rawcontent.available() == 0) + break; + + // if it was an empty line, we're done + if (len == 0) + break; + } + hdrSize = + (int)((SharedInputStream)rawcontent).getPosition(); + } + + // skipped the header, the message is what's left + msgSize = rawcontent.available(); + + rawData = new SoftReference<>(rawcontent); + } + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error fetching POP3 content", ex); + } + + /* + * We have a cached stream, but we need to return + * a fresh stream to read from the beginning and + * that can be safely closed. + */ + rawcontent = ((SharedInputStream)rawcontent).newStream( + skipHeader ? hdrSize : 0, -1); + return rawcontent; + } + + /** + * Produce the raw bytes of the content. The data is fetched using + * the POP3 RETR command. + * + * @see #contentStream + */ + @Override + protected synchronized InputStream getContentStream() + throws MessagingException { + if (contentStream != null) + return ((SharedInputStream)contentStream).newStream(0, -1); + + InputStream cstream = getRawStream(true); + + /* + * Keep a hard reference to the data if we're using a file + * cache or if the "mail.pop3.keepmessagecontent" prop is set. + */ + TempFile cache = folder.getFileCache(); + if (cache != null || + ((POP3Store)(folder.getStore())).keepMessageContent) + contentStream = ((SharedInputStream)cstream).newStream(0, -1); + return cstream; + } + + /** + * Return the MIME format stream corresponding to this message part. + * + * @return the MIME format stream + * @since JavaMail 1.4.5 + */ + @Override + public InputStream getMimeStream() throws MessagingException { + return getRawStream(false); + } + + /** + * Invalidate the cache of content for this message object, causing + * it to be fetched again from the server the next time it is needed. + * If invalidateHeaders is true, invalidate the headers + * as well. + * + * @param invalidateHeaders invalidate the headers as well? + */ + public synchronized void invalidate(boolean invalidateHeaders) { + content = null; + InputStream rstream = rawData.get(); + if (rstream != null) { + // note that if the content is in the file cache, it will be lost + // and fetched from the server if it's needed again + try { + rstream.close(); + } catch (IOException ex) { + // ignore it + } + rawData = new SoftReference<>(null); + } + if (contentStream != null) { + try { + contentStream.close(); + } catch (IOException ex) { + // ignore it + } + contentStream = null; + } + msgSize = -1; + if (invalidateHeaders) { + headers = null; + hdrSize = -1; + } + } + + /** + * Fetch the header of the message and the first n lines + * of the raw content of the message. The headers and data are + * available in the returned InputStream. + * + * @param n number of lines of content to fetch + * @return InputStream containing the message headers and n content lines + * @exception MessagingException for failures + */ + public InputStream top(int n) throws MessagingException { + try { + synchronized (this) { + return folder.getProtocol().top(msgnum, n); + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error getting size", ex); + } + } + + /** + * Get all the headers for this header_name. Note that certain + * headers may be encoded as per RFC 2047 if they contain + * non US-ASCII characters and these should be decoded.

+ * + * @param name name of header + * @return array of headers + * @exception MessagingException for failures + * @see javax.mail.internet.MimeUtility + */ + @Override + public String[] getHeader(String name) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getHeader(name); + } + + /** + * Get all the headers for this header name, returned as a single + * String, with headers separated by the delimiter. If the + * delimiter is null, only the first header is + * returned. + * + * @param name the name of this header + * @param delimiter delimiter between returned headers + * @return the value fields for all headers with + * this name + * @exception MessagingException for failures + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getHeader(name, delimiter); + } + + /** + * Set the value for this header_name. Throws IllegalWriteException + * because POP3 messages are read-only. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @exception MessagingException for other failures + */ + @Override + public void setHeader(String name, String value) + throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Add this value to the existing values for this header_name. + * Throws IllegalWriteException because POP3 messages are read-only. + * + * @param name header name + * @param value header value + * @see javax.mail.internet.MimeUtility + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + */ + @Override + public void addHeader(String name, String value) + throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Remove all headers with this name. + * Throws IllegalWriteException because POP3 messages are read-only. + * + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + */ + @Override + public void removeHeader(String name) + throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Return all the headers from this Message as an enumeration + * of Header objects.

+ * + * Note that certain headers may be encoded as per RFC 2047 + * if they contain non US-ASCII characters and these should + * be decoded.

+ * + * @return array of header objects + * @exception MessagingException for failures + * @see javax.mail.internet.MimeUtility + */ + @Override + public Enumeration

getAllHeaders() throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getAllHeaders(); + } + + /** + * Return matching headers from this Message as an Enumeration of + * Header objects. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration
getMatchingHeaders(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getMatchingHeaders(names); + } + + /** + * Return non-matching headers from this Message as an + * Enumeration of Header objects. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration
getNonMatchingHeaders(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getNonMatchingHeaders(names); + } + + /** + * Add a raw RFC822 header-line. + * Throws IllegalWriteException because POP3 messages are read-only. + * + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + */ + @Override + public void addHeaderLine(String line) throws MessagingException { + // XXX - should check for read-only folder? + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Get all header lines as an Enumeration of Strings. A Header + * line is a raw RFC822 header-line, containing both the "name" + * and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration getAllHeaderLines() throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getAllHeaderLines(); + } + + /** + * Get matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC822 header-line, containing both + * the "name" and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getMatchingHeaderLines(names); + } + + /** + * Get non-matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC822 header-line, containing both + * the "name" and "value" field. + * + * @exception MessagingException for failures + */ + @Override + public Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException { + if (headers == null) + loadHeaders(); + return headers.getNonMatchingHeaderLines(names); + } + + /** + * POP3 message can't be changed. This method throws + * IllegalWriteException. + * + * @exception IllegalWriteException because the underlying + * implementation does not support modification + */ + @Override + public void saveChanges() throws MessagingException { + // POP3 Messages are read-only + throw new IllegalWriteException("POP3 messages are read-only"); + } + + /** + * Output the message as an RFC 822 format stream, without + * specified headers. If the property "mail.pop3.cachewriteto" + * is set to "true", and ignoreList is null, and the message hasn't + * already been cached as a side effect of other operations, the message + * content is cached before being written. Otherwise, the message is + * streamed directly to the output stream without being cached. + * + * @exception IOException if an error occurs writing to the stream + * or if an error is generated by the + * javax.activation layer. + * @exception MessagingException for other failures + * @see javax.activation.DataHandler#writeTo + */ + @Override + public synchronized void writeTo(OutputStream os, String[] ignoreList) + throws IOException, MessagingException { + InputStream rawcontent = rawData.get(); + if (rawcontent == null && ignoreList == null && + !((POP3Store)(folder.getStore())).cacheWriteTo) { + if (folder.logger.isLoggable(Level.FINE)) + folder.logger.fine("streaming msg " + msgnum); + if (!folder.getProtocol().retr(msgnum, os)) { + expunged = true; + throw new MessageRemovedException("can't retrieve message #" + + msgnum + " in POP3Message.writeTo"); // XXX - what else? + } + } else if (rawcontent != null && ignoreList == null) { + // can just copy the cached data + InputStream in = ((SharedInputStream)rawcontent).newStream(0, -1); + try { + byte[] buf = new byte[16*1024]; + int len; + while ((len = in.read(buf)) > 0) + os.write(buf, 0, len); + } finally { + try { + if (in != null) + in.close(); + } catch (IOException ex) { } + } + } else + super.writeTo(os, ignoreList); + } + + /** + * Load the headers for this message into the InternetHeaders object. + * The headers are fetched using the POP3 TOP command. + */ + private void loadHeaders() throws MessagingException { + assert !Thread.holdsLock(this); + try { + boolean fetchContent = false; + synchronized (this) { + if (headers != null) // check again under lock + return; + InputStream hdrs = null; + if (((POP3Store)(folder.getStore())).disableTop || + (hdrs = folder.getProtocol().top(msgnum, 0)) == null) { + // possibly because the TOP command isn't supported, + // load headers as a side effect of loading the entire + // content. + fetchContent = true; + } else { + try { + hdrSize = hdrs.available(); + headers = new InternetHeaders(hdrs); + } finally { + hdrs.close(); + } + } + } + + /* + * Outside the synchronization block... + * + * Do we need to fetch the entire mesage content in order to + * load the headers as a side effect? Yes, there's a race + * condition here - multiple threads could decide that the + * content needs to be fetched. Fortunately, they'll all + * synchronize in the getContentStream method and the content + * will only be loaded once. + */ + if (fetchContent) { + InputStream cs = null; + try { + cs = getContentStream(); + } finally { + if (cs != null) + cs.close(); + } + } + } catch (EOFException eex) { + folder.close(false); + throw new FolderClosedException(folder, eex.toString()); + } catch (IOException ex) { + throw new MessagingException("error loading POP3 headers", ex); + } + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3Provider.java b/app/src/main/java/com/sun/mail/pop3/POP3Provider.java new file mode 100644 index 0000000000..cf73a6c333 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3Provider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The POP3 protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class POP3Provider extends Provider { + public POP3Provider() { + super(Provider.Type.STORE, "pop3", POP3Store.class.getName(), + "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3SSLProvider.java b/app/src/main/java/com/sun/mail/pop3/POP3SSLProvider.java new file mode 100644 index 0000000000..4831c50e86 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3SSLProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The POP3 SSL protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class POP3SSLProvider extends Provider { + public POP3SSLProvider() { + super(Provider.Type.STORE, "pop3s", POP3SSLStore.class.getName(), + "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3SSLStore.java b/app/src/main/java/com/sun/mail/pop3/POP3SSLStore.java new file mode 100644 index 0000000000..fa0054c508 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3SSLStore.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import javax.mail.*; + +/** + * A POP3 Message Store using SSL. Contains only one folder, "INBOX". + * + * @author Bill Shannon + */ +public class POP3SSLStore extends POP3Store { + + public POP3SSLStore(Session session, URLName url) { + super(session, url, "pop3s", true); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/POP3Store.java b/app/src/main/java/com/sun/mail/pop3/POP3Store.java new file mode 100644 index 0000000000..2c8330af9b --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/POP3Store.java @@ -0,0 +1,547 @@ +/* + * Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.util.Locale; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.lang.reflect.*; + +import javax.mail.*; +import javax.mail.internet.*; +import java.io.File; +import java.io.PrintStream; +import java.io.IOException; +import java.io.EOFException; +import java.util.Collections; +import java.util.Map; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketConnectException; +import com.sun.mail.util.MailConnectException; + +/** + * A POP3 Message Store. Contains only one folder, "INBOX". + * + * See the com.sun.mail.pop3 package + * documentation for further information on the POP3 protocol provider.

+ * + * @author Bill Shannon + * @author John Mani + */ +public class POP3Store extends Store { + + private String name = "pop3"; // my protocol name + private int defaultPort = 110; // default POP3 port + private boolean isSSL = false; // use SSL? + + private Protocol port = null; // POP3 port for self + private POP3Folder portOwner = null; // folder owning port + private String host = null; // host + private int portNum = -1; + private String user = null; + private String passwd = null; + private boolean useStartTLS = false; + private boolean requireStartTLS = false; + private boolean usingSSL = false; + private Map capabilities; + private MailLogger logger; + + // following set here and accessed by other classes in this package + volatile Constructor messageConstructor = null; + volatile boolean rsetBeforeQuit = false; + volatile boolean disableTop = false; + volatile boolean forgetTopHeaders = false; + volatile boolean supportsUidl = true; + volatile boolean cacheWriteTo = false; + volatile boolean useFileCache = false; + volatile File fileCacheDir = null; + volatile boolean keepMessageContent = false; + volatile boolean finalizeCleanClose = false; + + public POP3Store(Session session, URLName url) { + this(session, url, "pop3", false); + } + + public POP3Store(Session session, URLName url, + String name, boolean isSSL) { + super(session, url); + if (url != null) + name = url.getProtocol(); + this.name = name; + logger = new MailLogger(this.getClass(), "DEBUG POP3", + session.getDebug(), session.getDebugOut()); + + if (!isSSL) + isSSL = PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".ssl.enable", false); + if (isSSL) + this.defaultPort = 995; + else + this.defaultPort = 110; + this.isSSL = isSSL; + + rsetBeforeQuit = getBoolProp("rsetbeforequit"); + disableTop = getBoolProp("disabletop"); + forgetTopHeaders = getBoolProp("forgettopheaders"); + cacheWriteTo = getBoolProp("cachewriteto"); + useFileCache = getBoolProp("filecache.enable"); + String dir = session.getProperty("mail." + name + ".filecache.dir"); + if (dir != null && logger.isLoggable(Level.CONFIG)) + logger.config("mail." + name + ".filecache.dir: " + dir); + if (dir != null) + fileCacheDir = new File(dir); + keepMessageContent = getBoolProp("keepmessagecontent"); + + // mail.pop3.starttls.enable enables use of STLS command + useStartTLS = getBoolProp("starttls.enable"); + + // mail.pop3.starttls.required requires use of STLS command + requireStartTLS = getBoolProp("starttls.required"); + + // mail.pop3.finalizecleanclose requires clean close when finalizing + finalizeCleanClose = getBoolProp("finalizecleanclose"); + + String s = session.getProperty("mail." + name + ".message.class"); + if (s != null) { + logger.log(Level.CONFIG, "message class: {0}", s); + try { + ClassLoader cl = this.getClass().getClassLoader(); + + // now load the class + Class messageClass = null; + try { + // First try the "application's" class loader. + // This should eventually be replaced by + // Thread.currentThread().getContextClassLoader(). + messageClass = Class.forName(s, false, cl); + } catch (ClassNotFoundException ex1) { + // That didn't work, now try the "system" class loader. + // (Need both of these because JDK 1.1 class loaders + // may not delegate to their parent class loader.) + messageClass = Class.forName(s); + } + + Class[] c = {javax.mail.Folder.class, int.class}; + messageConstructor = messageClass.getConstructor(c); + } catch (Exception ex) { + logger.log(Level.CONFIG, "failed to load message class", ex); + } + } + } + + /** + * Get the value of a boolean property. + * Print out the value if logging is enabled. + */ + private final synchronized boolean getBoolProp(String prop) { + prop = "mail." + name + "." + prop; + boolean val = PropUtil.getBooleanProperty(session.getProperties(), + prop, false); + if (logger.isLoggable(Level.CONFIG)) + logger.config(prop + ": " + val); + return val; + } + + /** + * Get a reference to the session. + */ + synchronized Session getSession() { + return session; + } + + @Override + protected synchronized boolean protocolConnect(String host, int portNum, + String user, String passwd) throws MessagingException { + + // check for non-null values of host, password, user + if (host == null || passwd == null || user == null) + return false; + + // if port is not specified, set it to value of mail.pop3.port + // property if it exists, otherwise default to 110 + if (portNum == -1) + portNum = PropUtil.getIntProperty(session.getProperties(), + "mail." + name + ".port", -1); + + if (portNum == -1) + portNum = defaultPort; + + this.host = host; + this.portNum = portNum; + this.user = user; + this.passwd = passwd; + try { + port = getPort(null); + } catch (EOFException eex) { + throw new AuthenticationFailedException(eex.getMessage()); + } catch (SocketConnectException scex) { + throw new MailConnectException(scex); + } catch (IOException ioex) { + throw new MessagingException("Connect failed", ioex); + } + + return true; + } + + /** + * Check whether this store is connected. Override superclass + * method, to actually ping our server connection. + */ + /* + * Note that we maintain somewhat of an illusion of being connected + * even if we're not really connected. This is because a Folder + * can use the connection and close it when it's done. If we then + * ask whether the Store's connected we want the answer to be true, + * as long as we can reconnect at that point. This means that we + * need to be able to reconnect the Store on demand. + */ + @Override + public synchronized boolean isConnected() { + if (!super.isConnected()) + // if we haven't been connected at all, don't bother with + // the NOOP. + return false; + try { + if (port == null) + port = getPort(null); + else if (!port.noop()) + throw new IOException("NOOP failed"); + return true; + } catch (IOException ioex) { + // no longer connected, close it down + try { + super.close(); // notifies listeners + } catch (MessagingException mex) { + // ignore it + } + return false; + } + } + + synchronized Protocol getPort(POP3Folder owner) throws IOException { + Protocol p; + + // if we already have a port, remember who's using it + if (port != null && portOwner == null) { + portOwner = owner; + return port; + } + + // need a new port, create it and try to login + p = new Protocol(host, portNum, logger, + session.getProperties(), "mail." + name, isSSL); + + if (useStartTLS || requireStartTLS) { + if (p.hasCapability("STLS")) { + if (p.stls()) { + // success, refresh capabilities + p.setCapabilities(p.capa()); + } else if (requireStartTLS) { + logger.fine("STLS required but failed"); + throw cleanupAndThrow(p, + new EOFException("STLS required but failed")); + } + } else if (requireStartTLS) { + logger.fine("STLS required but not supported"); + throw cleanupAndThrow(p, + new EOFException("STLS required but not supported")); + } + } + + capabilities = p.getCapabilities(); // save for later, may be null + usingSSL = p.isSSL(); // in case anyone asks + + /* + * If we haven't explicitly disabled use of the TOP command, + * and the server has provided its capabilities, + * and the server doesn't support the TOP command, + * disable the TOP command. + */ + if (!disableTop && + capabilities != null && !capabilities.containsKey("TOP")) { + disableTop = true; + logger.fine("server doesn't support TOP, disabling it"); + } + + supportsUidl = capabilities == null || capabilities.containsKey("UIDL"); + + try { + if (!authenticate(p, user, passwd)) + throw cleanupAndThrow(p, new EOFException("login failed")); + } catch (EOFException ex) { + throw cleanupAndThrow(p, ex); + } catch (Exception ex) { + throw cleanupAndThrow(p, new EOFException(ex.getMessage())); + } + + + /* + * If a Folder closes the port, and then a Folder + * is opened, the Store won't have a port. In that + * case, the getPort call will come from Folder.open, + * but we need to keep track of the port in the Store + * so that a later call to Folder.isOpen, which calls + * Store.isConnected, will use the same port. + */ + if (port == null && owner != null) { + port = p; + portOwner = owner; + } + if (portOwner == null) + portOwner = owner; + return p; + } + + private static IOException cleanupAndThrow(Protocol p, IOException ife) { + try { + p.quit(); + } catch (Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + return ife; + } + + /** + * Authenticate to the server. + * + * XXX - This extensible authentication mechanism scheme was adapted + * from the SMTPTransport class. The work was done at the last + * minute for the 1.6.5 release and so is not as clean as it + * could be. There's great confusion over boolean success/failure + * return codes vs exceptions. This should all be cleaned up at + * some point, and more testing should be done, but I'm leaving + * it in this "I believe it works" state for now. I've tested + * it with LOGIN, PLAIN, and XOAUTH2 mechanisms, the latter being + * the primary motivation for the work right now. + * + * @param p the Protocol object to use + * @param user the user to authenticate as + * @param passwd the password for the user + * @return true if authentication succeeds + * @exception MessagingException if authentication fails + * @since Jakarta Mail 1.6.5 + */ + private boolean authenticate(Protocol p, String user, String passwd) + throws MessagingException { + // setting mail.pop3.auth.mechanisms controls which mechanisms will + // be used, and in what order they'll be considered. only the first + // match is used. + String mechs = session.getProperty("mail." + name + ".auth.mechanisms"); + boolean usingDefaultMechs = false; + if (mechs == null) { + mechs = p.getDefaultMechanisms(); + usingDefaultMechs = true; + } + + String authzid = + session.getProperty("mail." + name + ".sasl.authorizationid"); + if (authzid == null) + authzid = user; + /* + * XXX - maybe someday + * + if (enableSASL) { + logger.fine("Authenticate with SASL"); + try { + if (sasllogin(getSASLMechanisms(), getSASLRealm(), authzid, + user, passwd)) { + return true; // success + } else { + logger.fine("SASL authentication failed"); + return false; + } + } catch (UnsupportedOperationException ex) { + logger.log(Level.FINE, "SASL support failed", ex); + // if the SASL support fails, fall back to non-SASL + } + } + */ + + if (logger.isLoggable(Level.FINE)) + logger.fine("Attempt to authenticate using mechanisms: " + mechs); + + /* + * Loop through the list of mechanisms supplied by the user + * (or defaulted) and try each in turn. If the server supports + * the mechanism and we have an authenticator for the mechanism, + * and it hasn't been disabled, use it. + */ + StringTokenizer st = new StringTokenizer(mechs); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + m = m.toUpperCase(Locale.ENGLISH); + if (!p.supportsMechanism(m)) { + logger.log(Level.FINE, "no authenticator for mechanism {0}", m); + continue; + } + + if (!p.supportsAuthentication(m)) { + logger.log(Level.FINE, "mechanism {0} not supported by server", + m); + continue; + } + + /* + * If using the default mechanisms, check if this one is disabled. + */ + if (usingDefaultMechs) { + String dprop = "mail." + name + ".auth." + + m.toLowerCase(Locale.ENGLISH) + ".disable"; + boolean disabled = PropUtil.getBooleanProperty( + session.getProperties(), + dprop, !p.isMechanismEnabled(m)); + if (disabled) { + if (logger.isLoggable(Level.FINE)) + logger.fine("mechanism " + m + + " disabled by property: " + dprop); + continue; + } + } + + // only the first supported and enabled mechanism is used + logger.log(Level.FINE, "Using mechanism {0}", m); + String msg = + p.authenticate(m, host, authzid, user, passwd); + if (msg != null) + throw new AuthenticationFailedException(msg); + return true; + } + + // if no authentication mechanism found, fail + throw new AuthenticationFailedException( + "No authentication mechanisms supported by both server and client"); + } + + private static boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + synchronized void closePort(POP3Folder owner) { + if (portOwner == owner) { + port = null; + portOwner = null; + } + } + + @Override + public synchronized void close() throws MessagingException { + close(false); + } + + synchronized void close(boolean force) throws MessagingException { + try { + if (port != null) { + if (force) + port.close(); + else + port.quit(); + } + } catch (IOException ioex) { + } finally { + port = null; + + // to set the state and send the closed connection event + super.close(); + } + } + + @Override + public Folder getDefaultFolder() throws MessagingException { + checkConnected(); + return new DefaultFolder(this); + } + + /** + * Only the name "INBOX" is supported. + */ + @Override + public Folder getFolder(String name) throws MessagingException { + checkConnected(); + return new POP3Folder(this, name); + } + + @Override + public Folder getFolder(URLName url) throws MessagingException { + checkConnected(); + return new POP3Folder(this, url.getFile()); + } + + /** + * Return a Map of the capabilities the server provided, + * as per RFC 2449. If the server doesn't support RFC 2449, + * an emtpy Map is returned. The returned Map can not be modified. + * The key to the Map is the upper case capability name as + * a String. The value of the entry is the entire String + * capability line returned by the server.

+ * + * For example, to check if the server supports the STLS capability, use: + * if (store.capabilities().containsKey("STLS")) ... + * + * @return Map of capabilities + * @exception MessagingException for failures + * @since JavaMail 1.4.3 + */ + public Map capabilities() throws MessagingException { + Map c; + synchronized (this) { + c = capabilities; + } + if (c != null) + return Collections.unmodifiableMap(c); + else + return Collections.emptyMap(); + } + + /** + * Is this POP3Store using SSL to connect to the server? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public synchronized boolean isSSL() { + return usingSSL; + } + + @Override + protected void finalize() throws Throwable { + try { + if (port != null) // don't force a connection attempt + close(!finalizeCleanClose); + } finally { + super.finalize(); + } + } + + private void checkConnected() throws MessagingException { + if (!super.isConnected()) + throw new MessagingException("Not connected"); + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/Protocol.java b/app/src/main/java/com/sun/mail/pop3/Protocol.java new file mode 100644 index 0000000000..fb0a05aaa5 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/Protocol.java @@ -0,0 +1,1279 @@ +/* + * Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.util.*; +import java.net.*; +import java.io.*; +import java.security.*; +import java.util.logging.Level; +import java.nio.charset.StandardCharsets; +import javax.net.ssl.SSLSocket; + +import com.sun.mail.auth.Ntlm; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64DecoderStream; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.SocketFetcher; +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.TraceInputStream; +import com.sun.mail.util.TraceOutputStream; +import com.sun.mail.util.SharedByteArrayOutputStream; + +class Response { + boolean ok = false; // true if "+OK" + boolean cont = false; // true if "+ " continuation line + String data = null; // rest of line after "+OK" or "-ERR" + InputStream bytes = null; // all the bytes from a multi-line response +} + +/** + * This class provides a POP3 connection and implements + * the POP3 protocol requests. + * + * APOP support courtesy of "chamness". + * + * @author Bill Shannon + */ +class Protocol { + private Socket socket; // POP3 socket + private String host; // host we're connected to + private Properties props; // session properties + private String prefix; // protocol name prefix, for props + private BufferedReader input; // input buf + private PrintWriter output; // output buf + private TraceInputStream traceInput; + private TraceOutputStream traceOutput; + private MailLogger logger; + private MailLogger traceLogger; + private String apopChallenge = null; + private Map capabilities = null; + private boolean pipelining; + private boolean noauthdebug = true; // hide auth info in debug output + private boolean traceSuspended; // temporarily suspend tracing + private Map authenticators = new HashMap<>(); + private String defaultAuthenticationMechanisms; // set in constructor + private String localHostName; + + private static final int POP3_PORT = 110; // standard POP3 port + private static final String CRLF = "\r\n"; + // sometimes the returned size isn't quite big enough + private static final int SLOP = 128; + + /** + * Open a connection to the POP3 server. + */ + Protocol(String host, int port, MailLogger logger, + Properties props, String prefix, boolean isSSL) + throws IOException { + this.host = host; + this.props = props; + this.prefix = prefix; + this.logger = logger; + traceLogger = logger.getSubLogger("protocol", null); + noauthdebug = !PropUtil.getBooleanProperty(props, + "mail.debug.auth", false); + + Response r; + boolean enableAPOP = getBoolProp(props, prefix + ".apop.enable"); + boolean disableCapa = getBoolProp(props, prefix + ".disablecapa"); + try { + if (port == -1) + port = POP3_PORT; + if (logger.isLoggable(Level.FINE)) + logger.fine("connecting to host \"" + host + + "\", port " + port + ", isSSL " + isSSL); + + socket = SocketFetcher.getSocket(host, port, props, prefix, isSSL); + initStreams(); + r = simpleCommand(null); + } catch (IOException ioe) { + throw cleanupAndThrow(socket, ioe); + } + + if (!r.ok) { + throw cleanupAndThrow(socket, new IOException("Connect failed")); + } + if (enableAPOP && r.data != null) { + int challStart = r.data.indexOf('<'); // start of challenge + int challEnd = r.data.indexOf('>', challStart); // end of challenge + if (challStart != -1 && challEnd != -1) + apopChallenge = r.data.substring(challStart, challEnd + 1); + logger.log(Level.FINE, "APOP challenge: {0}", apopChallenge); + } + + // if server supports RFC 2449, set capabilities + if (!disableCapa) + setCapabilities(capa()); + + pipelining = hasCapability("PIPELINING") || + PropUtil.getBooleanProperty(props, prefix + ".pipelining", false); + if (pipelining) + logger.config("PIPELINING enabled"); + + // created here, because they're inner classes that reference "this" + Authenticator[] a = new Authenticator[] { + new LoginAuthenticator(), + new PlainAuthenticator(), + //new DigestMD5Authenticator(), + new NtlmAuthenticator(), + new OAuth2Authenticator() + }; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < a.length; i++) { + authenticators.put(a[i].getMechanism(), a[i]); + sb.append(a[i].getMechanism()).append(' '); + } + defaultAuthenticationMechanisms = sb.toString(); + } + + private static IOException cleanupAndThrow(Socket socket, IOException ife) { + try { + socket.close(); + } catch (Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + return ife; + } + + private static boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + /** + * Get the value of a boolean property. + * Print out the value if logging is enabled. + */ + private final synchronized boolean getBoolProp(Properties props, + String prop) { + boolean val = PropUtil.getBooleanProperty(props, prop, false); + if (logger.isLoggable(Level.CONFIG)) + logger.config(prop + ": " + val); + return val; + } + + private void initStreams() throws IOException { + boolean quote = PropUtil.getBooleanProperty(props, + "mail.debug.quote", false); + traceInput = + new TraceInputStream(socket.getInputStream(), traceLogger); + traceInput.setQuote(quote); + + traceOutput = + new TraceOutputStream(socket.getOutputStream(), traceLogger); + traceOutput.setQuote(quote); + + // should be US-ASCII, but not all JDK's support it so use iso-8859-1 + input = new BufferedReader(new InputStreamReader(traceInput, + "iso-8859-1")); + output = new PrintWriter( + new BufferedWriter( + new OutputStreamWriter(traceOutput, "iso-8859-1"))); + } + + @Override + protected void finalize() throws Throwable { + try { + if (socket != null) // Forgot to logout ?! + quit(); + } finally { + super.finalize(); + } + } + + /** + * Parse the capabilities from a CAPA response. + */ + synchronized void setCapabilities(InputStream in) { + if (in == null) { + capabilities = null; + return; + } + + capabilities = new HashMap<>(10); + BufferedReader r = null; + try { + r = new BufferedReader(new InputStreamReader(in, "us-ascii")); + } catch (UnsupportedEncodingException ex) { + // should never happen + assert false; + } + String s; + try { + while ((s = r.readLine()) != null) { + String cap = s; + int i = cap.indexOf(' '); + if (i > 0) + cap = cap.substring(0, i); + capabilities.put(cap.toUpperCase(Locale.ENGLISH), s); + } + } catch (IOException ex) { + // should never happen + } finally { + try { + in.close(); + } catch (IOException ex) { } + } + } + + /** + * Check whether the given capability is supported by + * this server. Returns true if so, otherwise + * returns false. + */ + synchronized boolean hasCapability(String c) { + return capabilities != null && + capabilities.containsKey(c.toUpperCase(Locale.ENGLISH)); + } + + /** + * Return the map of capabilities returned by the server. + */ + synchronized Map getCapabilities() { + return capabilities; + } + + /** + * Does this Protocol object support the named authentication mechanism? + * + * @since Jakarta Mail 1.6.5 + */ + boolean supportsMechanism(String mech) { + return authenticators.containsKey(mech.toUpperCase(Locale.ENGLISH)); + } + + /** + * Return the whitespace separated string list of default authentication + * mechanisms. + * + * @since Jakarta Mail 1.6.5 + */ + String getDefaultMechanisms() { + return defaultAuthenticationMechanisms; + } + + /** + * Is the named authentication mechanism enabled? + * + * @since Jakarta Mail 1.6.5 + */ + boolean isMechanismEnabled(String mech) { + Authenticator a = authenticators.get(mech.toUpperCase(Locale.ENGLISH)); + return a != null && a.enabled(); + } + + /** + * Authenticate to the server using the named authentication mechanism + * and the supplied credentials. + * + * @since Jakarta Mail 1.6.5 + */ + synchronized String authenticate(String mech, + String host, String authzid, + String user, String passwd) { + Authenticator a = authenticators.get(mech.toUpperCase(Locale.ENGLISH)); + if (a == null) + return "No such authentication mechanism: " + mech; + try { + if (!a.authenticate(host, authzid, user, passwd)) + return "login failed"; + return null; + } catch (IOException ex) { + return ex.getMessage(); + } + } + + /** + * Does the server we're connected to support the specified + * authentication mechanism? Uses the information + * returned by the server from the CAPA command. + * + * @param auth the authentication mechanism + * @return true if the authentication mechanism is supported + * + * @since Jakarta Mail 1.6.5 + */ + synchronized boolean supportsAuthentication(String auth) { + assert Thread.holdsLock(this); + if (auth.equals("LOGIN")) + return true; + if (capabilities == null) + return false; + String a = capabilities.get("SASL"); + if (a == null) + return false; + StringTokenizer st = new StringTokenizer(a); + while (st.hasMoreTokens()) { + String tok = st.nextToken(); + if (tok.equalsIgnoreCase(auth)) + return true; + } + return false; + } + + /** + * Login to the server, using the USER and PASS commands. + */ + synchronized String login(String user, String password) + throws IOException { + Response r; + // only pipeline password if connection is secure + boolean batch = pipelining && socket instanceof SSLSocket; + + try { + + if (noauthdebug && isTracing()) { + logger.fine("authentication command trace suppressed"); + suspendTracing(); + } + String dpw = null; + if (apopChallenge != null) + dpw = getDigest(password); + if (apopChallenge != null && dpw != null) { + r = simpleCommand("APOP " + user + " " + dpw); + } else if (batch) { + String cmd = "USER " + user; + batchCommandStart(cmd); + issueCommand(cmd); + cmd = "PASS " + password; + batchCommandContinue(cmd); + issueCommand(cmd); + r = readResponse(); + if (!r.ok) { + String err = r.data != null ? r.data : "USER command failed"; + readResponse(); // read and ignore PASS response + batchCommandEnd(); + return err; + } + r = readResponse(); + batchCommandEnd(); + } else { + r = simpleCommand("USER " + user); + if (!r.ok) + return r.data != null ? r.data : "USER command failed"; + r = simpleCommand("PASS " + password); + } + if (noauthdebug && isTracing()) + logger.log(Level.FINE, "authentication command {0}", + (r.ok ? "succeeded" : "failed")); + if (!r.ok) + return r.data != null ? r.data : "login failed"; + return null; + + } finally { + resumeTracing(); + } + } + + /** + * Gets the APOP message digest. + * From RFC 1939: + * + * The 'digest' parameter is calculated by applying the MD5 + * algorithm [RFC1321] to a string consisting of the timestamp + * (including angle-brackets) followed by a shared secret. + * The 'digest' parameter itself is a 16-octet value which is + * sent in hexadecimal format, using lower-case ASCII characters. + * + * @param password The APOP password + * @return The APOP digest or an empty string if an error occurs. + */ + private String getDigest(String password) { + String key = apopChallenge + password; + byte[] digest; + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + digest = md.digest(key.getBytes("iso-8859-1")); // XXX + } catch (NoSuchAlgorithmException nsae) { + return null; + } catch (UnsupportedEncodingException uee) { + return null; + } + return toHex(digest); + } + + /** + * Abstract base class for POP3 authentication mechanism implementations. + * + * @since Jakarta Mail 1.6.5 + */ + private abstract class Authenticator { + protected Response resp; // the response, used by subclasses + private final String mech; // the mechanism name, set in the constructor + private final boolean enabled; // is this mechanism enabled by default? + + Authenticator(String mech) { + this(mech, true); + } + + Authenticator(String mech, boolean enabled) { + this.mech = mech.toUpperCase(Locale.ENGLISH); + this.enabled = enabled; + } + + String getMechanism() { + return mech; + } + + boolean enabled() { + return enabled; + } + + /** + * Start the authentication handshake by issuing the AUTH command. + * Delegate to the doAuth method to do the mechanism-specific + * part of the handshake. + */ + boolean authenticate(String host, String authzid, + String user, String passwd) throws IOException { + Throwable thrown = null; + try { + // use "initial response" capability, if supported + String ir = getInitialResponse(host, authzid, user, passwd); + if (noauthdebug && isTracing()) { + logger.fine("AUTH " + mech + " command trace suppressed"); + suspendTracing(); + } + if (ir != null) + resp = simpleCommand("AUTH " + mech + " " + + (ir.length() == 0 ? "=" : ir)); + else + resp = simpleCommand("AUTH " + mech); + + if (resp.cont) + doAuth(host, authzid, user, passwd); + } catch (IOException ex) { // should never happen, ignore + logger.log(Level.FINE, "AUTH " + mech + " failed", ex); + } catch (Throwable t) { // crypto can't be initialized? + logger.log(Level.FINE, "AUTH " + mech + " failed", t); + thrown = t; + } finally { + if (noauthdebug && isTracing()) + logger.fine("AUTH " + mech + " " + + (resp.ok ? "succeeded" : "failed")); + resumeTracing(); + if (!resp.ok) { + close(); + if (thrown != null) { + if (thrown instanceof Error) + throw (Error)thrown; + if (thrown instanceof Exception) { + EOFException ex = new EOFException( + resp.data != null ? + resp.data : "authentication failed"); + ex.initCause(thrown); + throw ex; + } + assert false : "unknown Throwable"; // can't happen + } + throw new EOFException(resp.data != null ? + resp.data : "authentication failed"); + } + } + return true; + } + + /** + * Provide the initial response to use in the AUTH command, + * or null if not supported. Subclasses that support the + * initial response capability will override this method. + */ + String getInitialResponse(String host, String authzid, String user, + String passwd) throws IOException { + return null; + } + + abstract void doAuth(String host, String authzid, String user, + String passwd) throws IOException; + } + + /** + * Perform the authentication handshake for LOGIN authentication. + * + * @since Jakarta Mail 1.6.5 + */ + private class LoginAuthenticator extends Authenticator { + LoginAuthenticator() { + super("LOGIN"); + } + + @Override + boolean authenticate(String host, String authzid, + String user, String passwd) throws IOException { + String msg = null; + if ((msg = login(user, passwd)) != null) { + throw new EOFException(msg); + } + return true; + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws IOException { + // should never get here + throw new EOFException("LOGIN asked for more"); + } + } + + /** + * Perform the authentication handshake for PLAIN authentication. + * + * @since Jakarta Mail 1.6.5 + */ + private class PlainAuthenticator extends Authenticator { + PlainAuthenticator() { + super("PLAIN"); + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws IOException { + // return "authziduserpasswd" + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = + new BASE64EncoderStream(bos, Integer.MAX_VALUE); + if (authzid != null) + b64os.write(authzid.getBytes(StandardCharsets.UTF_8)); + b64os.write(0); + b64os.write(user.getBytes(StandardCharsets.UTF_8)); + b64os.write(0); + b64os.write(passwd.getBytes(StandardCharsets.UTF_8)); + b64os.flush(); // complete the encoding + + return ASCIIUtility.toString(bos.toByteArray()); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws IOException { + // should never get here + throw new EOFException("PLAIN asked for more"); + } + } + + /** + * Perform the authentication handshake for DIGEST-MD5 authentication. + * + * @since Jakarta Mail 1.6.5 + */ + /* + * XXX - Need to move DigestMD5 class to com.sun.mail.auth + * + private class DigestMD5Authenticator extends Authenticator { + private DigestMD5 md5support; // only create if needed + + DigestMD5Authenticator() { + super("DIGEST-MD5"); + } + + private synchronized DigestMD5 getMD5() { + if (md5support == null) + md5support = new DigestMD5(logger); + return md5support; + } + + @Override + void doAuth(Protocol p, String host, String authzid, + String user, String passwd) + throws IOException { + DigestMD5 md5 = getMD5(); + assert md5 != null; + + byte[] b = md5.authClient(host, user, passwd, getSASLRealm(), + resp.data); + resp = p.simpleCommand(b); + if (resp.cont) { // client authenticated by server + if (!md5.authServer(resp.data)) { + // server NOT authenticated by client !!! + resp.ok = false; + } else { + // send null response + resp = simpleCommand(new byte[0]); + } + } + } + } + */ + + /** + * Perform the authentication handshake for NTLM authentication. + * + * @since Jakarta Mail 1.6.5 + */ + private class NtlmAuthenticator extends Authenticator { + private Ntlm ntlm; + + NtlmAuthenticator() { + super("NTLM"); + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws IOException { + ntlm = new Ntlm(props.getProperty(prefix + ".auth.ntlm.domain"), + getLocalHost(), user, passwd, logger); + + int flags = PropUtil.getIntProperty( + props, prefix + ".auth.ntlm.flags", 0); + boolean v2 = PropUtil.getBooleanProperty( + props, prefix + ".auth.ntlm.v2", true); + + String type1 = ntlm.generateType1Msg(flags, v2); + return type1; + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws IOException { + assert ntlm != null; + String type3 = ntlm.generateType3Msg( + resp.data.substring(4).trim()); + + resp = simpleCommand(type3); + } + } + + /** + * Perform the authentication handshake for XOAUTH2 authentication. + * + * @since Jakarta Mail 1.6.5 + */ + private class OAuth2Authenticator extends Authenticator { + + OAuth2Authenticator() { + super("XOAUTH2", false); // disabled by default + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws IOException { + String resp = "user=" + user + "\001auth=Bearer " + + passwd + "\001\001"; + byte[] b = BASE64EncoderStream.encode( + resp.getBytes(StandardCharsets.UTF_8)); + return ASCIIUtility.toString(b); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws IOException { + // OAuth2 failure returns a JSON error code, + // which looks like a "please continue" to the authenticate() + // code, so we turn that into a clean failure here. + String err = ""; + if (resp.data != null) { + byte[] b = resp.data.getBytes(StandardCharsets.UTF_8); + b = BASE64DecoderStream.decode(b); + err = new String(b, StandardCharsets.UTF_8); + } + throw new EOFException("OAUTH2 authentication failed: " + err); + } + } + + /** + * Get the name of the local host. + * + * @return the local host name + * @since Jakarta Mail 1.6.5 + */ + private synchronized String getLocalHost() { + // get our hostname and cache it for future use + try { + if (localHostName == null || localHostName.length() == 0) { + InetAddress localHost = InetAddress.getLocalHost(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } catch (UnknownHostException uhex) { + } + + // last chance, try to get our address from our socket + if (localHostName == null || localHostName.length() <= 0) { + if (socket != null && socket.isBound()) { + InetAddress localHost = socket.getLocalAddress(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } + return localHostName; + } + + private static char[] digits = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + /** + * Convert a byte array to a string of hex digits representing the bytes. + */ + private static String toHex(byte[] bytes) { + char[] result = new char[bytes.length * 2]; + + for (int index = 0, i = 0; index < bytes.length; index++) { + int temp = bytes[index] & 0xFF; + result[i++] = digits[temp >> 4]; + result[i++] = digits[temp & 0xF]; + } + return new String(result); + } + + /** + * Close down the connection, sending the QUIT command. + */ + synchronized boolean quit() throws IOException { + boolean ok = false; + try { + Response r = simpleCommand("QUIT"); + ok = r.ok; + } finally { + close(); + } + return ok; + } + + /** + * Close the connection without sending any commands. + */ + void close() { + try { + if (socket != null) + socket.close(); + } catch (IOException ex) { + // ignore it + } finally { + socket = null; + input = null; + output = null; + } + } + + /** + * Return the total number of messages and mailbox size, + * using the STAT command. + */ + synchronized Status stat() throws IOException { + Response r = simpleCommand("STAT"); + Status s = new Status(); + + /* + * Normally the STAT command shouldn't fail but apparently it + * does when accessing Hotmail too often, returning: + * -ERR login allowed only every 15 minutes + * (Why it doesn't just fail the login, I don't know.) + * This is a serious failure that we don't want to hide + * from the user. + */ + if (!r.ok) + throw new IOException("STAT command failed: " + r.data); + + if (r.data != null) { + try { + StringTokenizer st = new StringTokenizer(r.data); + s.total = Integer.parseInt(st.nextToken()); + s.size = Integer.parseInt(st.nextToken()); + } catch (RuntimeException e) { + } + } + return s; + } + + /** + * Return the size of the message using the LIST command. + */ + synchronized int list(int msg) throws IOException { + Response r = simpleCommand("LIST " + msg); + int size = -1; + if (r.ok && r.data != null) { + try { + StringTokenizer st = new StringTokenizer(r.data); + st.nextToken(); // skip message number + size = Integer.parseInt(st.nextToken()); + } catch (RuntimeException e) { + // ignore it + } + } + return size; + } + + /** + * Return the size of all messages using the LIST command. + */ + synchronized InputStream list() throws IOException { + Response r = multilineCommand("LIST", 128); // 128 == output size est + return r.bytes; + } + + /** + * Retrieve the specified message. + * Given an estimate of the message's size we can be more efficient, + * preallocating the array and returning a SharedInputStream to allow + * us to share the array. + */ + synchronized InputStream retr(int msg, int size) throws IOException { + Response r; + String cmd; + boolean batch = size == 0 && pipelining; + if (batch) { + cmd = "LIST " + msg; + batchCommandStart(cmd); + issueCommand(cmd); + cmd = "RETR " + msg; + batchCommandContinue(cmd); + issueCommand(cmd); + r = readResponse(); + if (r.ok && r.data != null) { + // parse the LIST response to get the message size + try { + StringTokenizer st = new StringTokenizer(r.data); + st.nextToken(); // skip message number + size = Integer.parseInt(st.nextToken()); + // don't allow ridiculous sizes + if (size > 1024*1024*1024 || size < 0) + size = 0; + else { + if (logger.isLoggable(Level.FINE)) + logger.fine("pipeline message size " + size); + size += SLOP; + } + } catch (RuntimeException e) { + } + } + r = readResponse(); + if (r.ok) + r.bytes = readMultilineResponse(size + SLOP); + batchCommandEnd(); + } else { + cmd = "RETR " + msg; + multilineCommandStart(cmd); + issueCommand(cmd); + r = readResponse(); + if (!r.ok) { + multilineCommandEnd(); + return null; + } + + /* + * Many servers return a response to the RETR command of the form: + * +OK 832 octets + * If we don't have a size guess already, try to parse the response + * for data in that format and use it if found. It's only a guess, + * but it might be a good guess. + */ + if (size <= 0 && r.data != null) { + try { + StringTokenizer st = new StringTokenizer(r.data); + String s = st.nextToken(); + String octets = st.nextToken(); + if (octets.equals("octets")) { + size = Integer.parseInt(s); + // don't allow ridiculous sizes + if (size > 1024*1024*1024 || size < 0) + size = 0; + else { + if (logger.isLoggable(Level.FINE)) + logger.fine("guessing message size: " + size); + size += SLOP; + } + } + } catch (RuntimeException e) { + } + } + r.bytes = readMultilineResponse(size); + multilineCommandEnd(); + } + if (r.ok) { + if (size > 0 && logger.isLoggable(Level.FINE)) + logger.fine("got message size " + r.bytes.available()); + } + return r.bytes; + } + + /** + * Retrieve the specified message and stream the content to the + * specified OutputStream. Return true on success. + */ + synchronized boolean retr(int msg, OutputStream os) throws IOException { + String cmd = "RETR " + msg; + multilineCommandStart(cmd); + issueCommand(cmd); + Response r = readResponse(); + if (!r.ok) { + multilineCommandEnd(); + return false; + } + + Throwable terr = null; + int b, lastb = '\n'; + try { + while ((b = input.read()) >= 0) { + if (lastb == '\n' && b == '.') { + b = input.read(); + if (b == '\r') { + // end of response, consume LF as well + b = input.read(); + break; + } + } + + /* + * Keep writing unless we get an error while writing, + * which we defer until all of the data has been read. + */ + if (terr == null) { + try { + os.write(b); + } catch (IOException ex) { + logger.log(Level.FINE, "exception while streaming", ex); + terr = ex; + } catch (RuntimeException ex) { + logger.log(Level.FINE, "exception while streaming", ex); + terr = ex; + } + } + lastb = b; + } + } catch (InterruptedIOException iioex) { + /* + * As above in simpleCommand, close the socket to recover. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw iioex; + } + if (b < 0) + throw new EOFException("EOF on socket"); + + // was there a deferred error? + if (terr != null) { + if (terr instanceof IOException) + throw (IOException)terr; + if (terr instanceof RuntimeException) + throw (RuntimeException)terr; + assert false; // can't get here + } + multilineCommandEnd(); + return true; + } + + /** + * Return the message header and the first n lines of the message. + */ + synchronized InputStream top(int msg, int n) throws IOException { + Response r = multilineCommand("TOP " + msg + " " + n, 0); + return r.bytes; + } + + /** + * Delete (permanently) the specified message. + */ + synchronized boolean dele(int msg) throws IOException { + Response r = simpleCommand("DELE " + msg); + return r.ok; + } + + /** + * Return the UIDL string for the message. + */ + synchronized String uidl(int msg) throws IOException { + Response r = simpleCommand("UIDL " + msg); + if (!r.ok) + return null; + int i = r.data.indexOf(' '); + if (i > 0) + return r.data.substring(i + 1); + else + return null; + } + + /** + * Return the UIDL strings for all messages. + * The UID for msg #N is returned in uids[N-1]. + */ + synchronized boolean uidl(String[] uids) throws IOException { + Response r = multilineCommand("UIDL", 15 * uids.length); + if (!r.ok) + return false; + LineInputStream lis = new LineInputStream(r.bytes); + String line = null; + while ((line = lis.readLine()) != null) { + int i = line.indexOf(' '); + if (i < 1 || i >= line.length()) + continue; + int n = Integer.parseInt(line.substring(0, i)); + if (n > 0 && n <= uids.length) + uids[n - 1] = line.substring(i + 1); + } + try { + r.bytes.close(); + } catch (IOException ex) { + // ignore it + } + return true; + } + + /** + * Do a NOOP. + */ + synchronized boolean noop() throws IOException { + Response r = simpleCommand("NOOP"); + return r.ok; + } + + /** + * Do an RSET. + */ + synchronized boolean rset() throws IOException { + Response r = simpleCommand("RSET"); + return r.ok; + } + + /** + * Start TLS using STLS command specified by RFC 2595. + * If already using SSL, this is a nop and the STLS command is not issued. + */ + synchronized boolean stls() throws IOException { + if (socket instanceof SSLSocket) + return true; // nothing to do + Response r = simpleCommand("STLS"); + if (r.ok) { + // it worked, now switch the socket into TLS mode + try { + socket = SocketFetcher.startTLS(socket, host, props, prefix); + initStreams(); + } catch (IOException ioex) { + try { + socket.close(); + } finally { + socket = null; + input = null; + output = null; + } + IOException sioex = + new IOException("Could not convert socket to TLS"); + sioex.initCause(ioex); + throw sioex; + } + } + return r.ok; + } + + /** + * Is this connection using SSL? + */ + synchronized boolean isSSL() { + return socket instanceof SSLSocket; + } + + /** + * Get server capabilities using CAPA command specified by RFC 2449. + * Returns null if not supported. + */ + synchronized InputStream capa() throws IOException { + Response r = multilineCommand("CAPA", 128); // 128 == output size est + if (!r.ok) + return null; + return r.bytes; + } + + /** + * Issue a simple POP3 command and return the response. + */ + private Response simpleCommand(String cmd) throws IOException { + simpleCommandStart(cmd); + issueCommand(cmd); + Response r = readResponse(); + simpleCommandEnd(); + return r; + } + + /** + * Send the specified command. + */ + private void issueCommand(String cmd) throws IOException { + if (socket == null) + throw new IOException("Folder is closed"); // XXX + + if (cmd != null) { + cmd += CRLF; + output.print(cmd); // do it in one write + output.flush(); + } + } + + /** + * Read the response to a command. + */ + private Response readResponse() throws IOException { + String line = null; + try { + line = input.readLine(); + } catch (InterruptedIOException iioex) { + /* + * If we get a timeout while using the socket, we have no idea + * what state the connection is in. The server could still be + * alive, but slow, and could still be sending data. The only + * safe way to recover is to drop the connection. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw new EOFException(iioex.getMessage()); + } catch (SocketException ex) { + /* + * If we get an error while using the socket, we have no idea + * what state the connection is in. The server could still be + * alive, but slow, and could still be sending data. The only + * safe way to recover is to drop the connection. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw new EOFException(ex.getMessage()); + } + + if (line == null) { + traceLogger.finest(""); + throw new EOFException("EOF on socket"); + } + Response r = new Response(); + if (line.startsWith("+OK")) + r.ok = true; + else if (line.startsWith("+ ")) { + r.ok = true; + r.cont = true; + } else if (line.startsWith("-ERR")) + r.ok = false; + else + throw new IOException("Unexpected response: " + line); + int i; + if ((i = line.indexOf(' ')) >= 0) + r.data = line.substring(i + 1); + return r; + } + + /** + * Issue a POP3 command that expects a multi-line response. + * size is an estimate of the response size. + */ + private Response multilineCommand(String cmd, int size) throws IOException { + multilineCommandStart(cmd); + issueCommand(cmd); + Response r = readResponse(); + if (!r.ok) { + multilineCommandEnd(); + return r; + } + r.bytes = readMultilineResponse(size); + multilineCommandEnd(); + return r; + } + + /** + * Read the response to a multiline command after the command response. + * The size parameter indicates the expected size of the response; + * the actual size can be different. Returns an InputStream to the + * response bytes. + */ + private InputStream readMultilineResponse(int size) throws IOException { + SharedByteArrayOutputStream buf = new SharedByteArrayOutputStream(size); + int b, lastb = '\n'; + try { + while ((b = input.read()) >= 0) { + if (lastb == '\n' && b == '.') { + b = input.read(); + if (b == '\r') { + // end of response, consume LF as well + b = input.read(); + break; + } + } + buf.write(b); + lastb = b; + } + } catch (InterruptedIOException iioex) { + /* + * As above in readResponse, close the socket to recover. + */ + try { + socket.close(); + } catch (IOException cex) { } + throw iioex; + } + if (b < 0) + throw new EOFException("EOF on socket"); + return buf.toStream(); + } + + /** + * Is protocol tracing enabled? + */ + protected boolean isTracing() { + return traceLogger.isLoggable(Level.FINEST); + } + + /** + * Temporarily turn off protocol tracing, e.g., to prevent + * tracing the authentication sequence, including the password. + */ + private void suspendTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(false); + traceOutput.setTrace(false); + } + } + + /** + * Resume protocol tracing, if it was enabled to begin with. + */ + private void resumeTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(true); + traceOutput.setTrace(true); + } + } + + /* + * Probe points for GlassFish monitoring. + */ + private void simpleCommandStart(String command) { } + private void simpleCommandEnd() { } + private void multilineCommandStart(String command) { } + private void multilineCommandEnd() { } + private void batchCommandStart(String command) { } + private void batchCommandContinue(String command) { } + private void batchCommandEnd() { } +} diff --git a/app/src/main/java/com/sun/mail/pop3/Status.java b/app/src/main/java/com/sun/mail/pop3/Status.java new file mode 100644 index 0000000000..bbfbd9cd9c --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/Status.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +/** + * Result of POP3 STAT command. + */ +class Status { + int total = 0; // number of messages in the mailbox + int size = 0; // size of the mailbox +}; diff --git a/app/src/main/java/com/sun/mail/pop3/TempFile.java b/app/src/main/java/com/sun/mail/pop3/TempFile.java new file mode 100644 index 0000000000..0d769e4aa5 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/TempFile.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.*; + +/** + * A temporary file used to cache POP3 messages. + */ +class TempFile { + + private File file; // the temp file name + private WritableSharedFile sf; + + /** + * Create a temp file in the specified directory (if not null). + * The file will be deleted when the JVM exits. + */ + public TempFile(File dir) throws IOException { + file = File.createTempFile("pop3.", ".mbox", dir); + // XXX - need JDK 6 to set permissions on the file to owner-only + file.deleteOnExit(); + sf = new WritableSharedFile(file); + } + + /** + * Return a stream for appending to the temp file. + */ + public AppendStream getAppendStream() throws IOException { + return sf.getAppendStream(); + } + + /** + * Close and remove this temp file. + */ + public void close() { + try { + sf.close(); + } catch (IOException ex) { + // ignore it + } + file.delete(); + } + + @Override + protected void finalize() throws Throwable { + try { + close(); + } finally { + super.finalize(); + } + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/WritableSharedFile.java b/app/src/main/java/com/sun/mail/pop3/WritableSharedFile.java new file mode 100644 index 0000000000..8daa0c5378 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/WritableSharedFile.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.pop3; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; +import javax.mail.util.SharedFileInputStream; + +/** + * A subclass of SharedFileInputStream that also allows writing. + */ +class WritableSharedFile extends SharedFileInputStream { + + private RandomAccessFile raf; + private AppendStream af; + + public WritableSharedFile(File file) throws IOException { + super(file); + try { + raf = new RandomAccessFile(file, "rw"); + } catch (IOException ex) { + // if anything goes wrong opening the writable file, + // close the readable file too + super.close(); + } + } + + /** + * Return the writable version of this file. + */ + public RandomAccessFile getWritableFile() { + return raf; + } + + /** + * Close the readable and writable files. + */ + @Override + public void close() throws IOException { + try { + super.close(); + } finally { + raf.close(); + } + } + + /** + * Update the size of the readable file after writing to the file. Updates + * the length to be the current size of the file. + */ + synchronized long updateLength() throws IOException { + datalen = in.length(); + af = null; + return datalen; + } + + /** + * Return a new AppendStream, but only if one isn't in active use. + */ + public synchronized AppendStream getAppendStream() throws IOException { + if (af != null) { + throw new IOException( + "POP3 file cache only supports single threaded access"); + } + af = new AppendStream(this); + return af; + } +} diff --git a/app/src/main/java/com/sun/mail/pop3/package.html b/app/src/main/java/com/sun/mail/pop3/package.html new file mode 100644 index 0000000000..0a01a78be0 --- /dev/null +++ b/app/src/main/java/com/sun/mail/pop3/package.html @@ -0,0 +1,767 @@ + + + + + + +com.sun.mail.pop3 package + + + +A POP3 protocol provider for the Jakarta Mail API +that provides access to a POP3 message store. +Refer to +RFC 1939 +for more information. +

+The POP3 provider provides a Store object that contains a single Folder +named "INBOX". Due to the limitations of the POP3 protocol, many of +the Jakarta Mail API capabilities like event notification, folder management, +flag management, etc. are not allowed. The corresponding methods throw +the MethodNotSupportedException exception; see below for details. +

+

+Note that Jakarta Mail does not include a local store into +which messages can be downloaded and stored. See our + +Third Party Products +web page for availability of "mbox" and "MH" local store providers. +

+

+The POP3 provider is accessed through the Jakarta Mail APIs by using the protocol +name "pop3" or a URL of the form "pop3://user:password@host:port/INBOX". +

+

+POP3 supports only a single folder named "INBOX". +

+

+POP3 supports no permanent flags (see +{@link javax.mail.Folder#getPermanentFlags Folder.getPermanentFlags()}). +In particular, the Flags.Flag.RECENT flag will never be set +for POP3 +messages. It's up to the application to determine which messages in a +POP3 mailbox are "new". There are several strategies to accomplish +this, depending on the needs of the application and the environment: +

+
    +
  • +A simple approach would be to keep track of the newest +message seen by the application. +
  • +
  • +An alternative would be to keep track of the UIDs (see below) +of all messages that have been seen. +
  • +
  • +Another approach is to download all messages into a local +mailbox, so that all messages in the POP3 mailbox are, by +definition, new. +
  • +
+

+All approaches will require some permanent storage associated with the client. +

+

+POP3 does not support the Folder.expunge() method. To delete and +expunge messages, set the Flags.Flag.DELETED flag on the messages +and close the folder using the Folder.close(true) method. You +cannot expunge without closing the folder. +

+

+POP3 does not provide a "received date", so the getReceivedDate +method will return null. +It may be possible to examine other message headers (e.g., the +"Received" headers) to estimate the received date, but these techniques +are error-prone at best. +

+

+The POP3 provider supports the POP3 UIDL command, see +{@link com.sun.mail.pop3.POP3Folder#getUID POP3Folder.getUID()}. +You can use it as follows: +

+
+if (folder instanceof com.sun.mail.pop3.POP3Folder) {
+    com.sun.mail.pop3.POP3Folder pf =
+	(com.sun.mail.pop3.POP3Folder)folder;
+    String uid = pf.getUID(msg);
+    if (uid != null)
+	... // use it
+}
+
+

+You can also pre-fetch all the UIDs for all messages like this: +

+
+FetchProfile fp = new FetchProfile();
+fp.add(UIDFolder.FetchProfileItem.UID);
+folder.fetch(folder.getMessages(), fp);
+
+

+Then use the technique above to get the UID for each message. This is +similar to the technique used with the UIDFolder interface supported by +IMAP, but note that POP3 UIDs are strings, not integers like IMAP +UIDs. See the POP3 spec for details. +

+

+When the headers of a POP3 message are accessed, the POP3 provider uses +the TOP command to fetch all headers, which are then cached. Use of the +TOP command can be disabled with the mail.pop3.disabletop +property, in which case the entire message content is fetched with the +RETR command. +

+

+When the content of a POP3 message is accessed, the POP3 provider uses +the RETR command to fetch the entire message. Normally the message +content is cached in memory. By setting the +mail.pop3.filecache.enable property, the message content +will instead be cached in a temporary file. The file will be removed +when the folder is closed. Caching message content in a file is generally +slower, but uses substantially less memory and may be helpful when dealing +with very large messages. +

+

+The {@link com.sun.mail.pop3.POP3Message#invalidate POP3Message.invalidate} +method can be used to invalidate cached data without closing the folder. +Note that if the file cache is being used the data in the file will be +forgotten and fetched from the server if it's needed again, and stored again +in the file cache. +

+

+The POP3 CAPA command (defined by +RFC 2449) +will be used to determine the capabilities supported by the server. +Some servers don't implement the CAPA command, and some servers don't +return correct information, so various properties are available to +disable use of certain POP3 commands, including CAPA. +

+

+If the server advertises the PIPELINING capability (defined by +RFC 2449), +or the mail.pop3.pipelining property is set, the POP3 +provider will send some commands in batches, which can significantly +improve performance and memory use. +Some servers that don't support the CAPA command or don't advertise +PIPELINING may still support pipelining; experimentation may be required. +

+

+If pipelining is supported and the connection is using +SSL, the USER and PASS commands will be sent as a batch. +(If SSL is not being used, the PASS command isn't sent +until the user is verified to avoid exposing the password +if the user name is bad.) +

+

+If pipelining is supported, when fetching a message with the RETR command, +the LIST command will be sent as well, and the result will be used to size +the I/O buffer, greatly reducing memory usage when fetching messages. +

+Properties +

+The POP3 protocol provider supports the following properties, +which may be set in the Jakarta Mail Session object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +

+
+	props.put("mail.pop3.port", "888");
+
+

+to set the mail.pop3.port property, which is of type int. +

+

+Note that if you're using the "pop3s" protocol to access POP3 over SSL, +all the properties would be named "mail.pop3s.*". +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
POP3 properties
NameTypeDescription
mail.pop3.userStringDefault user name for POP3.
mail.pop3.hostStringThe POP3 server to connect to.
mail.pop3.portintThe POP3 server port to connect to, if the connect() method doesn't +explicitly specify one. Defaults to 110.
mail.pop3.connectiontimeoutintSocket connection timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.pop3.timeoutintSocket read timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.pop3.writetimeoutintSocket write timeout value in milliseconds. +This timeout is implemented by using a +java.util.concurrent.ScheduledExecutorService per connection +that schedules a thread to close the socket if the timeout expires. +Thus, the overhead of using this timeout is one thread per connection. +Default is infinite timeout.
mail.pop3.rsetbeforequitboolean +Send a POP3 RSET command when closing the folder, before sending the +QUIT command. Useful with POP3 servers that implicitly mark all +messages that are read as "deleted"; this will prevent such messages +from being deleted and expunged unless the client requests so. Default +is false. +
mail.pop3.message.classString +Class name of a subclass of com.sun.mail.pop3.POP3Message. +The subclass can be used to handle (for example) non-standard +Content-Type headers. The subclass must have a public constructor +of the form MyPOP3Message(Folder f, int msgno) +throws MessagingException. +
mail.pop3.localaddressString +Local address (host name) to bind to when creating the POP3 socket. +Defaults to the address picked by the Socket class. +Should not normally need to be set, but useful with multi-homed hosts +where it's important to pick a particular local address to bind to. +
mail.pop3.localportint +Local port number to bind to when creating the POP3 socket. +Defaults to the port number picked by the Socket class. +
mail.pop3.apop.enableboolean +If set to true, use APOP instead of USER/PASS to login to the +POP3 server, if the POP3 server supports APOP. APOP sends a +digest of the password rather than the clear text password. +Defaults to false. +
mail.pop3.auth.mechanismsString +If set, lists the authentication mechanisms to consider, and the order +in which to consider them. Only mechanisms supported by the server and +supported by the current implementation will be used. +The default is "LOGIN PLAIN DIGEST-MD5 NTLM", which includes all +the authentication mechanisms supported by the current implementation +except XOAUTH2. +
mail.pop3.auth.login.disablebooleanIf true, prevents use of the USER and PASS +commands. +Default is false.
mail.pop3.auth.plain.disablebooleanIf true, prevents use of the AUTH PLAIN command. +Default is false.
mail.pop3.auth.digest-md5.disablebooleanIf true, prevents use of the AUTH DIGEST-MD5 command. +Default is false.
mail.pop3.auth.ntlm.disablebooleanIf true, prevents use of the AUTH NTLM command. +Default is false.
mail.pop3.auth.ntlm.domainString +The NTLM authentication domain. +
mail.pop3.auth.ntlm.flagsint +NTLM protocol-specific flags. +See +http://curl.haxx.se/rfc/ntlm.html#theNtlmFlags for details. +
mail.pop3.auth.xoauth2.disablebooleanIf true, prevents use of the AUTHENTICATE XOAUTH2 command. +Because the OAuth 2.0 protocol requires a special access token instead of +a password, this mechanism is disabled by default. Enable it by explicitly +setting this property to "false" or by setting the "mail.pop3.auth.mechanisms" +property to "XOAUTH2".
mail.pop3.socketFactorySocketFactory +If set to a class that implements the +javax.net.SocketFactory interface, this class +will be used to create POP3 sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.pop3.socketFactory.classString +If set, specifies the name of a class that implements the +javax.net.SocketFactory interface. This class +will be used to create POP3 sockets. +
mail.pop3.socketFactory.fallbackboolean +If set to true, failure to create a socket using the specified +socket factory class will cause the socket to be created using +the java.net.Socket class. +Defaults to true. +
mail.pop3.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.pop3.ssl.enableboolean +If set to true, use SSL to connect and use the SSL port by default. +Defaults to false for the "pop3" protocol and true for the "pop3s" protocol. +
mail.pop3.ssl.checkserveridentityboolean +If set to true, check the server identity as specified by +RFC 2595. +These additional checks based on the content of the server's certificate +are intended to prevent man-in-the-middle attacks. +Defaults to false. +
mail.pop3.ssl.trustString +If set, and a socket factory hasn't been specified, enables use of a +{@link com.sun.mail.util.MailSSLSocketFactory MailSSLSocketFactory}. +If set to "*", all hosts are trusted. +If set to a whitespace separated list of hosts, those hosts are trusted. +Otherwise, trust depends on the certificate the server presents. +
mail.pop3.ssl.socketFactorySSLSocketFactory +If set to a class that extends the +javax.net.ssl.SSLSocketFactory class, this class +will be used to create POP3 SSL sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.pop3.ssl.socketFactory.classString +If set, specifies the name of a class that extends the +javax.net.ssl.SSLSocketFactory class. This class +will be used to create POP3 SSL sockets. +
mail.pop3.ssl.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.pop3.ssl.protocolsstring +Specifies the SSL protocols that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledProtocols method. +
mail.pop3.ssl.ciphersuitesstring +Specifies the SSL cipher suites that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledCipherSuites method. +
mail.pop3.starttls.enableboolean +If true, enables the use of the STLS command (if +supported by the server) to switch the connection to a TLS-protected +connection before issuing any login commands. +If the server does not support STARTTLS, the connection continues without +the use of TLS; see the +mail.pop3.starttls.required +property to fail if STARTTLS isn't supported. +Note that an appropriate trust store must configured so that the client +will trust the server's certificate. +Defaults to false. +
mail.pop3.starttls.requiredboolean +If true, requires the use of the STLS command. +If the server doesn't support the STLS command, or the command +fails, the connect method will fail. +Defaults to false. +
mail.pop3.proxy.hoststring +Specifies the host name of an HTTP web proxy server that will be used for +connections to the mail server. +
mail.pop3.proxy.portstring +Specifies the port number for the HTTP web proxy server. +Defaults to port 80. +
mail.pop3.proxy.userstring +Specifies the user name to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.pop3.proxy.passwordstring +Specifies the password to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.pop3.socks.hoststring +Specifies the host name of a SOCKS5 proxy server that will be used for +connections to the mail server. +
mail.pop3.socks.portstring +Specifies the port number for the SOCKS5 proxy server. +This should only need to be used if the proxy server is not using +the standard port number of 1080. +
mail.pop3.disabletopboolean +If set to true, the POP3 TOP command will not be used to fetch +message headers. This is useful for POP3 servers that don't +properly implement the TOP command, or that provide incorrect +information in the TOP command results. +Defaults to false. +
mail.pop3.disablecapaboolean +If set to true, the POP3 CAPA command will not be used to fetch +server capabilities. This is useful for POP3 servers that don't +properly implement the CAPA command, or that provide incorrect +information in the CAPA command results. +Defaults to false. +
mail.pop3.forgettopheadersboolean +If set to true, the headers that might have been retrieved using +the POP3 TOP command will be forgotten and replaced by headers +retrieved as part of the POP3 RETR command. Some servers, such +as some versions of Microsft Exchange and IBM Lotus Notes, +will return slightly different +headers each time the TOP or RETR command is used. To allow the +POP3 provider to properly parse the message content returned from +the RETR command, the headers also returned by the RETR command +must be used. Setting this property to true will cause these +headers to be used, even if they differ from the headers returned +previously as a result of using the TOP command. +Defaults to false. +
mail.pop3.filecache.enableboolean +If set to true, the POP3 provider will cache message data in a temporary +file rather than in memory. Messages are only added to the cache when +accessing the message content. Message headers are always cached in +memory (on demand). The file cache is removed when the folder is closed +or the JVM terminates. +Defaults to false. +
mail.pop3.filecache.dirString +If the file cache is enabled, this property can be used to override the +default directory used by the JDK for temporary files. +
mail.pop3.cachewritetoboolean +Controls the behavior of the +{@link com.sun.mail.pop3.POP3Message#writeTo writeTo} method +on a POP3 message object. +If set to true, and the message content hasn't yet been cached, +and ignoreList is null, the message is cached before being written. +Otherwise, the message is streamed directly +to the output stream without being cached. +Defaults to false. +
mail.pop3.keepmessagecontentboolean +The content of a message is cached when it is first fetched. +Normally this cache uses a {@link java.lang.ref.SoftReference SoftReference} +to refer to the cached content. This allows the cached content to be purged +if memory is low, in which case the content will be fetched again if it's +needed. +If this property is set to true, a hard reference to the cached content +will be kept, preventing the memory from being reused until the folder +is closed or the cached content is explicitly invalidated (using the +{@link com.sun.mail.pop3.POP3Message#invalidate invalidate} method). +(This was the behavior in previous versions of Jakarta Mail.) +Defaults to false. +
mail.pop3.finalizecleancloseboolean +When the finalizer for POP3Store or POP3Folder is called, +should the connection to the server be closed cleanly, as if the +application called the close method? +Or should the connection to the server be closed without sending +any commands to the server? +Defaults to false, the connection is not closed cleanly. +
+

+In general, applications should not need to use the classes in this +package directly. Instead, they should use the APIs defined by +javax.mail package (and subpackages). Applications should +never construct instances of POP3Store or +POP3Folder directly. Instead, they should use the +Session method getStore to acquire an +appropriate Store object, and from that acquire +Folder objects. +

+

+In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, +the com.sun.mail.pop3 provider logs the same information using +{@link java.util.logging.Logger} as described in the following table: +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
POP3 Loggers
Logger NameLogging LevelPurpose
com.sun.mail.pop3CONFIGConfiguration of the POP3Store
com.sun.mail.pop3FINEGeneral debugging output
com.sun.mail.pop3.protocolFINESTComplete protocol trace
+ +

+WARNING: The APIs unique to this package should be +considered EXPERIMENTAL. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +

+ + + diff --git a/app/src/main/java/com/sun/mail/smtp/DigestMD5.java b/app/src/main/java/com/sun/mail/smtp/DigestMD5.java new file mode 100644 index 0000000000..401834a0ce --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/DigestMD5.java @@ -0,0 +1,231 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import java.util.*; +import java.util.logging.Level; +import java.security.*; +import java.nio.charset.StandardCharsets; + +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.BASE64DecoderStream; + +/** + * DIGEST-MD5 authentication support. + * + * @author Dean Gibson + * @author Bill Shannon + */ + +public class DigestMD5 { + + private MailLogger logger; + private MessageDigest md5; + private String uri; + private String clientResponse; + + public DigestMD5(MailLogger logger) { + this.logger = logger.getLogger(this.getClass(), "DEBUG DIGEST-MD5"); + logger.config("DIGEST-MD5 Loaded"); + } + + /** + * Return client's authentication response to server's challenge. + * + * @param host the host name + * @param user the user name + * @param passwd the user's password + * @param realm the security realm + * @param serverChallenge the challenge from the server + * @return byte array with client's response + * @exception IOException for I/O errors + */ + public byte[] authClient(String host, String user, String passwd, + String realm, String serverChallenge) + throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = new BASE64EncoderStream(bos, Integer.MAX_VALUE); + SecureRandom random; + try { + //random = SecureRandom.getInstance("SHA1PRNG"); + random = new SecureRandom(); + md5 = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException ex) { + logger.log(Level.FINE, "NoSuchAlgorithmException", ex); + throw new IOException(ex.toString()); + } + StringBuilder result = new StringBuilder(); + + uri = "smtp/" + host; + String nc = "00000001"; + String qop = "auth"; + byte[] bytes = new byte[32]; // arbitrary size ... + int resp; + + logger.fine("Begin authentication ..."); + + // Code based on http://www.ietf.org/rfc/rfc2831.txt + Map map = tokenize(serverChallenge); + + if (realm == null) { + String text = map.get("realm"); + realm = text != null ? new StringTokenizer(text, ",").nextToken() + : host; + } + + // server challenge random value + String nonce = map.get("nonce"); + + // Does server support UTF-8 usernames and passwords? + String charset = map.get("charset"); + boolean utf8 = charset != null && charset.equalsIgnoreCase("utf-8"); + + random.nextBytes(bytes); + b64os.write(bytes); + b64os.flush(); + + // client challenge random value + String cnonce = bos.toString("iso-8859-1"); // really ASCII? + bos.reset(); + + // DIGEST-MD5 computation, common portion (order critical) + if (utf8) { + String up = user + ":" + realm + ":" + passwd; + md5.update(md5.digest(up.getBytes(StandardCharsets.UTF_8))); + } else + md5.update(md5.digest( + ASCIIUtility.getBytes(user + ":" + realm + ":" + passwd))); + md5.update(ASCIIUtility.getBytes(":" + nonce + ":" + cnonce)); + clientResponse = toHex(md5.digest()) + + ":" + nonce + ":" + nc + ":" + cnonce + ":" + qop + ":"; + + // DIGEST-MD5 computation, client response (order critical) + md5.update(ASCIIUtility.getBytes("AUTHENTICATE:" + uri)); + md5.update(ASCIIUtility.getBytes(clientResponse + toHex(md5.digest()))); + + // build response text (order not critical) + result.append("username=\"" + user + "\""); + result.append(",realm=\"" + realm + "\""); + result.append(",qop=" + qop); + result.append(",nc=" + nc); + result.append(",nonce=\"" + nonce + "\""); + result.append(",cnonce=\"" + cnonce + "\""); + result.append(",digest-uri=\"" + uri + "\""); + if (utf8) + result.append(",charset=\"utf-8\""); + result.append(",response=" + toHex(md5.digest())); + + if (logger.isLoggable(Level.FINE)) + logger.fine("Response => " + result.toString()); + b64os.write(ASCIIUtility.getBytes(result.toString())); + b64os.flush(); + return bos.toByteArray(); + } + + /** + * Allow the client to authenticate the server based on its + * response. + * + * @param serverResponse the response that was received from the server + * @return true if server is authenticated + * @exception IOException for character conversion failures + */ + public boolean authServer(String serverResponse) throws IOException { + Map map = tokenize(serverResponse); + // DIGEST-MD5 computation, server response (order critical) + md5.update(ASCIIUtility.getBytes(":" + uri)); + md5.update(ASCIIUtility.getBytes(clientResponse + toHex(md5.digest()))); + String text = toHex(md5.digest()); + if (!text.equals(map.get("rspauth"))) { + if (logger.isLoggable(Level.FINE)) + logger.fine("Expected => rspauth=" + text); + return false; // server NOT authenticated by client !!! + } + return true; + } + + /** + * Tokenize a response from the server. + * + * @return Map containing key/value pairs from server + */ + @SuppressWarnings("fallthrough") + private Map tokenize(String serverResponse) + throws IOException { + Map map = new HashMap<>(); + byte[] bytes = serverResponse.getBytes("iso-8859-1"); // really ASCII? + String key = null; + int ttype; + StreamTokenizer tokens + = new StreamTokenizer( + new InputStreamReader( + new BASE64DecoderStream( + new ByteArrayInputStream(bytes, 4, bytes.length - 4) + ), "iso-8859-1" // really ASCII? + ) + ); + + tokens.ordinaryChars('0', '9'); // reset digits + tokens.wordChars('0', '9'); // digits may start words + while ((ttype = tokens.nextToken()) != StreamTokenizer.TT_EOF) { + switch (ttype) { + case StreamTokenizer.TT_WORD: + if (key == null) { + key = tokens.sval; + break; + } + // fall-thru + case '"': + if (logger.isLoggable(Level.FINE)) + logger.fine("Received => " + + key + "='" + tokens.sval + "'"); + if (map.containsKey(key)) { // concatenate multiple values + map.put(key, map.get(key) + "," + tokens.sval); + } else { + map.put(key, tokens.sval); + } + key = null; + break; + default: // XXX - should never happen? + break; + } + } + return map; + } + + private static char[] digits = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' + }; + + /** + * Convert a byte array to a string of hex digits representing the bytes. + */ + private static String toHex(byte[] bytes) { + char[] result = new char[bytes.length * 2]; + + for (int index = 0, i = 0; index < bytes.length; index++) { + int temp = bytes[index] & 0xFF; + result[i++] = digits[temp >> 4]; + result[i++] = digits[temp & 0xF]; + } + return new String(result); + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPAddressFailedException.java b/app/src/main/java/com/sun/mail/smtp/SMTPAddressFailedException.java new file mode 100644 index 0000000000..234820e7cb --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPAddressFailedException.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.SendFailedException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is thrown when the message cannot be sent.

+ * + * The exception includes the address to which the message could not be + * sent. This will usually appear in a chained list of exceptions, + * one per address, attached to a top level SendFailedException that + * aggregates all the addresses. + * + * @since JavaMail 1.3.2 + */ + +public class SMTPAddressFailedException extends SendFailedException { + protected InternetAddress addr; // address that failed + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = 804831199768630097L; + + /** + * Constructs an SMTPAddressFailedException with the specified + * address, return code, and error string. + * + * @param addr the address that failed + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the failure + * @param err the error string from the SMTP server + */ + public SMTPAddressFailedException(InternetAddress addr, String cmd, int rc, + String err) { + super(err); + this.addr = addr; + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the address that failed. + * + * @return the address + */ + public InternetAddress getAddress() { + return addr; + } + + /** + * Return the command that failed. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + + /** + * Return the return code from the SMTP server that indicates the + * reason for the failure. See + * RFC 821 + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPAddressSucceededException.java b/app/src/main/java/com/sun/mail/smtp/SMTPAddressSucceededException.java new file mode 100644 index 0000000000..2ddb29a7eb --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPAddressSucceededException.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.MessagingException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is chained off a SendFailedException when the + * mail.smtp.reportsuccess property is true. It + * indicates an address to which the message was sent. The command + * will be an SMTP RCPT command and the return code will be the + * return code from that command. + * + * @since JavaMail 1.3.2 + */ + +public class SMTPAddressSucceededException extends MessagingException { + protected InternetAddress addr; // address that succeeded + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = -1168335848623096749L; + + /** + * Constructs an SMTPAddressSucceededException with the specified + * address, return code, and error string. + * + * @param addr the address that succeeded + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the success + * @param err the error string from the SMTP server + */ + public SMTPAddressSucceededException(InternetAddress addr, + String cmd, int rc, String err) { + super(err); + this.addr = addr; + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the address that succeeded. + * + * @return the address + */ + public InternetAddress getAddress() { + return addr; + } + + /** + * Return the command that succeeded. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + + /** + * Return the return code from the SMTP server that indicates the + * reason for the success. See + * RFC 821 + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPMessage.java b/app/src/main/java/com/sun/mail/smtp/SMTPMessage.java new file mode 100644 index 0000000000..826ea44ce0 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPMessage.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import javax.mail.*; +import javax.mail.internet.*; + +/** + * This class is a specialization of the MimeMessage class that allows + * you to specify various SMTP options and parameters that will be + * used when this message is sent over SMTP. Simply use this class + * instead of MimeMessage and set SMTP options using the methods on + * this class.

+ * + * See the com.sun.mail.smtp package + * documentation for further information on the SMTP protocol provider.

+ * + * @author Bill Shannon + * @see javax.mail.internet.MimeMessage + */ + +public class SMTPMessage extends MimeMessage { + + /** Never notify of delivery status */ + public static final int NOTIFY_NEVER = -1; + /** Notify of delivery success */ + public static final int NOTIFY_SUCCESS = 1; + /** Notify of delivery failure */ + public static final int NOTIFY_FAILURE = 2; + /** Notify of delivery delay */ + public static final int NOTIFY_DELAY = 4; + + /** Return full message with delivery status notification */ + public static final int RETURN_FULL = 1; + /** Return only message headers with delivery status notification */ + public static final int RETURN_HDRS = 2; + + private static final String[] returnOptionString = { null, "FULL", "HDRS" }; + + private String envelopeFrom; // the string to use in the MAIL FROM: command + private int notifyOptions = 0; + private int returnOption = 0; + private boolean sendPartial = false; + private boolean allow8bitMIME = false; + private String submitter = null; // RFC 2554 AUTH=submitter + private String extension = null; // extensions to use with MAIL command + + /** + * Default constructor. An empty message object is created. + * The headers field is set to an empty InternetHeaders + * object. The flags field is set to an empty Flags + * object. The modified flag is set to true. + * + * @param session the Session + */ + public SMTPMessage(Session session) { + super(session); + } + + /** + * Constructs an SMTPMessage by reading and parsing the data from the + * specified MIME InputStream. The InputStream will be left positioned + * at the end of the data for the message. Note that the input stream + * parse is done within this constructor itself. + * + * @param session Session object for this message + * @param is the message input stream + * @exception MessagingException for failures + */ + public SMTPMessage(Session session, InputStream is) + throws MessagingException { + super(session, is); + } + + /** + * Constructs a new SMTPMessage with content initialized from the + * source MimeMessage. The new message is independent + * of the original.

+ * + * Note: The current implementation is rather inefficient, copying + * the data more times than strictly necessary. + * + * @param source the message to copy content from + * @exception MessagingException for failures + */ + public SMTPMessage(MimeMessage source) throws MessagingException { + super(source); + } + + /** + * Set the From address to appear in the SMTP envelope. Note that this + * is different than the From address that appears in the message itself. + * The envelope From address is typically used when reporting errors. + * See RFC 821 for + * details.

+ * + * If set, overrides the mail.smtp.from property. + * + * @param from the envelope From address + */ + public void setEnvelopeFrom(String from) { + envelopeFrom = from; + } + + /** + * Return the envelope From address. + * + * @return the envelope From address, or null if not set + */ + public String getEnvelopeFrom() { + return envelopeFrom; + } + + /** + * Set notification options to be used if the server supports + * Delivery Status Notification + * (RFC 1891). + * Either NOTIFY_NEVER or some combination of + * NOTIFY_SUCCESS, NOTIFY_FAILURE, and + * NOTIFY_DELAY.

+ * + * If set, overrides the mail.smtp.dsn.notify property. + * + * @param options notification options + */ + public void setNotifyOptions(int options) { + if (options < -1 || options >= 8) + throw new IllegalArgumentException("Bad return option"); + notifyOptions = options; + } + + /** + * Get notification options. Returns zero if no options set. + * + * @return notification options + */ + public int getNotifyOptions() { + return notifyOptions; + } + + /** + * Return notification options as an RFC 1891 string. + * Returns null if no options set. + */ + String getDSNNotify() { + if (notifyOptions == 0) + return null; + if (notifyOptions == NOTIFY_NEVER) + return "NEVER"; + StringBuilder sb = new StringBuilder(); + if ((notifyOptions & NOTIFY_SUCCESS) != 0) + sb.append("SUCCESS"); + if ((notifyOptions & NOTIFY_FAILURE) != 0) { + if (sb.length() != 0) + sb.append(','); + sb.append("FAILURE"); + } + if ((notifyOptions & NOTIFY_DELAY) != 0) { + if (sb.length() != 0) + sb.append(','); + sb.append("DELAY"); + } + return sb.toString(); + } + + /** + * Set return option to be used if server supports + * Delivery Status Notification + * (RFC 1891). + * Either RETURN_FULL or RETURN_HDRS.

+ * + * If set, overrides the mail.smtp.dsn.ret property. + * + * @param option return option + */ + public void setReturnOption(int option) { + if (option < 0 || option > RETURN_HDRS) + throw new IllegalArgumentException("Bad return option"); + returnOption = option; + } + + /** + * Return return option. Returns zero if no option set. + * + * @return return option + */ + public int getReturnOption() { + return returnOption; + } + + /** + * Return return option as an RFC 1891 string. + * Returns null if no option set. + */ + String getDSNRet() { + return returnOptionString[returnOption]; + } + + /** + * If set to true, and the server supports the 8BITMIME extension, text + * parts of this message that use the "quoted-printable" or "base64" + * encodings are converted to use "8bit" encoding if they follow the + * RFC 2045 rules for 8bit text.

+ * + * If true, overrides the mail.smtp.allow8bitmime property. + * + * @param allow allow 8-bit flag + */ + public void setAllow8bitMIME(boolean allow) { + allow8bitMIME = allow; + } + + /** + * Is use of the 8BITMIME extension is allowed? + * + * @return allow 8-bit flag + */ + public boolean getAllow8bitMIME() { + return allow8bitMIME; + } + + /** + * If set to true, and this message has some valid and some invalid + * addresses, send the message anyway, reporting the partial failure with + * a SendFailedException. If set to false (the default), the message is + * not sent to any of the recipients if there is an invalid recipient + * address.

+ * + * If true, overrides the mail.smtp.sendpartial property. + * + * @param partial send partial flag + */ + public void setSendPartial(boolean partial) { + sendPartial = partial; + } + + /** + * Send message if some addresses are invalid? + * + * @return send partial flag + */ + public boolean getSendPartial() { + return sendPartial; + } + + /** + * Gets the submitter to be used for the RFC 2554 AUTH= value + * in the MAIL FROM command. + * + * @return the name of the submitter. + */ + public String getSubmitter() { + return submitter; + } + + /** + * Sets the submitter to be used for the RFC 2554 AUTH= value + * in the MAIL FROM command. Normally only used by a server + * that's relaying a message. Clients will typically not + * set a submitter. See + * RFC 2554 + * for details. + * + * @param submitter the name of the submitter + */ + public void setSubmitter(String submitter) { + this.submitter = submitter; + } + + /** + * Gets the extension string to use with the MAIL command. + * + * @return the extension string + * + * @since JavaMail 1.3.2 + */ + public String getMailExtension() { + return extension; + } + + /** + * Set the extension string to use with the MAIL command. + * The extension string can be used to specify standard SMTP + * service extensions as well as vendor-specific extensions. + * Typically the application should use the + * {@link com.sun.mail.smtp.SMTPTransport SMTPTransport} + * method {@link com.sun.mail.smtp.SMTPTransport#supportsExtension + * supportsExtension} + * to verify that the server supports the desired service extension. + * See RFC 1869 + * and other RFCs that define specific extensions.

+ * + * For example: + * + *

+     * if (smtpTransport.supportsExtension("DELIVERBY"))
+     *    smtpMsg.setMailExtension("BY=60;R");
+     * 
+ * + * @param extension the extension string + * @since JavaMail 1.3.2 + */ + public void setMailExtension(String extension) { + this.extension = extension; + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPOutputStream.java b/app/src/main/java/com/sun/mail/smtp/SMTPOutputStream.java new file mode 100644 index 0000000000..d2fd75b911 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPOutputStream.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import com.sun.mail.util.CRLFOutputStream; + +/** + * In addition to converting lines into the canonical format, + * i.e., terminating lines with the CRLF sequence, escapes the "." + * by adding another "." to any "." that appears in the beginning + * of a line. See RFC821 section 4.5.2. + * + * @author Max Spivak + * @see CRLFOutputStream + */ +public class SMTPOutputStream extends CRLFOutputStream { + public SMTPOutputStream(OutputStream os) { + super(os); + } + + @Override + public void write(int b) throws IOException { + // if that last character was a newline, and the current + // character is ".", we always write out an extra ".". + if ((lastb == '\n' || lastb == '\r' || lastb == -1) && b == '.') { + out.write('.'); + } + + super.write(b); + } + + /* + * This method has been added to improve performance. + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + int lastc = (lastb == -1) ? '\n' : lastb; + int start = off; + + len += off; + for (int i = off; i < len; i++) { + if ((lastc == '\n' || lastc == '\r') && b[i] == '.') { + super.write(b, start, i - start); + out.write('.'); + start = i; + } + lastc = b[i]; + } + if ((len - start) > 0) + super.write(b, start, len - start); + } + + /** + * Override flush method in FilterOutputStream. + * + * The MimeMessage writeTo method flushes its buffer at the end, + * but we don't want to flush data out to the socket until we've + * also written the terminating "\r\n.\r\n". + * + * We buffer nothing so there's nothing to flush. We depend + * on the fact that CRLFOutputStream also buffers nothing. + * SMTPTransport will manually flush the socket before reading + * the response. + */ + @Override + public void flush() { + // do nothing + } + + /** + * Ensure we're at the beginning of a line. + * Write CRLF if not. + * + * @exception IOException if the write fails + */ + public void ensureAtBOL() throws IOException { + if (!atBOL) + super.writeln(); + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPProvider.java b/app/src/main/java/com/sun/mail/smtp/SMTPProvider.java new file mode 100644 index 0000000000..71f5703f43 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The SMTP protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class SMTPProvider extends Provider { + public SMTPProvider() { + super(Provider.Type.TRANSPORT, "smtp", SMTPTransport.class.getName(), + "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPSSLProvider.java b/app/src/main/java/com/sun/mail/smtp/SMTPSSLProvider.java new file mode 100644 index 0000000000..fe4d09e603 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPSSLProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.Provider; + +import com.sun.mail.util.DefaultProvider; + +/** + * The SMTP SSL protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class SMTPSSLProvider extends Provider { + public SMTPSSLProvider() { + super(Provider.Type.TRANSPORT, "smtps", + SMTPSSLTransport.class.getName(), "Oracle", null); + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPSSLTransport.java b/app/src/main/java/com/sun/mail/smtp/SMTPSSLTransport.java new file mode 100644 index 0000000000..f3a7300849 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPSSLTransport.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.*; + +/** + * This class implements the Transport abstract class using SMTP + * over SSL for message submission and transport. + * + * @author Bill Shannon + */ + +public class SMTPSSLTransport extends SMTPTransport { + + /** + * Constructor. + * + * @param session the Session + * @param urlname the URLName of this transport + */ + public SMTPSSLTransport(Session session, URLName urlname) { + super(session, urlname, "smtps", true); + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPSendFailedException.java b/app/src/main/java/com/sun/mail/smtp/SMTPSendFailedException.java new file mode 100644 index 0000000000..d8cf3e66a8 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPSendFailedException.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.Address; +import javax.mail.SendFailedException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is thrown when the message cannot be sent.

+ * + * This exception will usually appear first in a chained list of exceptions, + * followed by SMTPAddressFailedExceptions and/or + * SMTPAddressSucceededExceptions, * one per address. + * This exception corresponds to one of the SMTP commands used to + * send a message, such as the MAIL, DATA, and "end of data" commands, + * but not including the RCPT command. + * + * @since JavaMail 1.3.2 + */ + +public class SMTPSendFailedException extends SendFailedException { + protected InternetAddress addr; // address that failed + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = 8049122628728932894L; + + /** + * Constructs an SMTPSendFailedException with the specified + * address, return code, and error string. + * + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the failure + * @param err the error string from the SMTP server + * @param ex a chained exception + * @param vs the valid addresses the message was sent to + * @param vus the valid addresses the message was not sent to + * @param inv the invalid addresses + */ + public SMTPSendFailedException(String cmd, int rc, String err, Exception ex, + Address[] vs, Address[] vus, Address[] inv) { + super(err, ex, vs, vus, inv); + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the command that failed. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + /** + * Return the return code from the SMTP server that indicates the + * reason for the failure. See + * RFC 821 + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPSenderFailedException.java b/app/src/main/java/com/sun/mail/smtp/SMTPSenderFailedException.java new file mode 100644 index 0000000000..3139c79ff3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPSenderFailedException.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.SendFailedException; +import javax.mail.internet.InternetAddress; + +/** + * This exception is thrown when the message cannot be sent.

+ * + * The exception includes the sender's address, which the mail server + * rejected. + * + * @since JavaMail 1.4.4 + */ + +public class SMTPSenderFailedException extends SendFailedException { + protected InternetAddress addr; // address that failed + protected String cmd; // command issued to server + protected int rc; // return code from SMTP server + + private static final long serialVersionUID = 514540454964476947L; + + /** + * Constructs an SMTPSenderFailedException with the specified + * address, return code, and error string. + * + * @param addr the address that failed + * @param cmd the command that was sent to the SMTP server + * @param rc the SMTP return code indicating the failure + * @param err the error string from the SMTP server + */ + public SMTPSenderFailedException(InternetAddress addr, String cmd, int rc, + String err) { + super(err); + this.addr = addr; + this.cmd = cmd; + this.rc = rc; + } + + /** + * Return the address that failed. + * + * @return the address + */ + public InternetAddress getAddress() { + return addr; + } + + /** + * Return the command that failed. + * + * @return the command + */ + public String getCommand() { + return cmd; + } + + + /** + * Return the return code from the SMTP server that indicates the + * reason for the failure. See + * RFC 821 + * for interpretation of the return code. + * + * @return the return code + */ + public int getReturnCode() { + return rc; + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SMTPTransport.java b/app/src/main/java/com/sun/mail/smtp/SMTPTransport.java new file mode 100644 index 0000000000..efbab723ab --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SMTPTransport.java @@ -0,0 +1,2831 @@ +/* + * Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import java.io.*; +import java.net.*; +import java.util.*; +import java.util.logging.Level; +import java.lang.reflect.*; +import java.nio.charset.StandardCharsets; +import javax.net.ssl.SSLSocket; + +import javax.mail.*; +import javax.mail.event.*; +import javax.mail.internet.*; + +import com.sun.mail.util.PropUtil; +import com.sun.mail.util.MailLogger; +import com.sun.mail.util.ASCIIUtility; +import com.sun.mail.util.SocketFetcher; +import com.sun.mail.util.MailConnectException; +import com.sun.mail.util.SocketConnectException; +import com.sun.mail.util.BASE64EncoderStream; +import com.sun.mail.util.LineInputStream; +import com.sun.mail.util.TraceInputStream; +import com.sun.mail.util.TraceOutputStream; +import com.sun.mail.auth.Ntlm; + +/** + * This class implements the Transport abstract class using SMTP for + * message submission and transport.

+ * + * See the com.sun.mail.smtp package + * documentation for further information on the SMTP protocol provider.

+ * + * This class includes many protected methods that allow a subclass to + * extend this class and add support for non-standard SMTP commands. + * The {@link #issueCommand} and {@link #sendCommand} methods can be + * used to send simple SMTP commands. Other methods such as the + * {@link #mailFrom} and {@link #data} methods can be overridden to + * insert new commands before or after the corresponding SMTP commands. + * For example, a subclass could do this to send the XACT command + * before sending the DATA command: + *

+ *	protected OutputStream data() throws MessagingException {
+ *	    if (supportsExtension("XACCOUNTING"))
+ *	        issueCommand("XACT", 25);
+ *	    return super.data();
+ *	}
+ * 
+ * + * @author Max Spivak + * @author Bill Shannon + * @author Dean Gibson (DIGEST-MD5 authentication) + * @author Lu\u00EDs Serralheiro (NTLM authentication) + * + * @see javax.mail.event.ConnectionEvent + * @see javax.mail.event.TransportEvent + */ + +public class SMTPTransport extends Transport { + + private String name = "smtp"; // Name of this protocol + private int defaultPort = 25; // default SMTP port + private boolean isSSL = false; // use SSL? + private String host; // host we're connected to + + // Following fields valid only during the sendMessage method. + private MimeMessage message; // Message to be sent + private Address[] addresses; // Addresses to which to send the msg + // Valid sent, valid unsent and invalid addresses + private Address[] validSentAddr, validUnsentAddr, invalidAddr; + // Did we send the message even though some addresses were invalid? + private boolean sendPartiallyFailed = false; + // If so, here's an exception we need to throw + private MessagingException exception; + // stream where message data is written + private SMTPOutputStream dataStream; + + // Map of SMTP service extensions supported by server, if EHLO used. + private Hashtable extMap; + + private Map authenticators + = new HashMap<>(); + private String defaultAuthenticationMechanisms; // set in constructor + + private boolean quitWait = false; // true if we should wait + private boolean quitOnSessionReject = false; // true if we should send quit when session initiation is rejected + + private String saslRealm = UNKNOWN; + private String authorizationID = UNKNOWN; + private boolean enableSASL = false; // enable SASL authentication + private boolean useCanonicalHostName = false; // use canonical host name? + private String[] saslMechanisms = UNKNOWN_SA; + + private String ntlmDomain = UNKNOWN; // for ntlm authentication + + private boolean reportSuccess; // throw an exception even on success + private boolean useStartTLS; // use STARTTLS command + private boolean requireStartTLS; // require STARTTLS command + private boolean useRset; // use RSET instead of NOOP + private boolean noopStrict = true; // NOOP must return 250 for success + + private MailLogger logger; // debug logger + private MailLogger traceLogger; // protocol trace logger + private String localHostName; // our own host name + private String lastServerResponse; // last SMTP response + private int lastReturnCode; // last SMTP return code + private boolean notificationDone; // only notify once per send + + private SaslAuthenticator saslAuthenticator; // if SASL is being used + + private boolean noauthdebug = true; // hide auth info in debug output + private boolean debugusername; // include username in debug output? + private boolean debugpassword; // include password in debug output? + private boolean allowutf8; // allow UTF-8 usernames and passwords? + private int chunkSize; // chunk size if CHUNKING supported + + /** Headers that should not be included when sending */ + private static final String[] ignoreList = { "Bcc", "Content-Length" }; + private static final byte[] CRLF = { (byte)'\r', (byte)'\n' }; + private static final String UNKNOWN = "UNKNOWN"; // place holder + private static final String[] UNKNOWN_SA = new String[0]; // place holder + + /** + * Constructor that takes a Session object and a URLName + * that represents a specific SMTP server. + * + * @param session the Session + * @param urlname the URLName of this transport + */ + public SMTPTransport(Session session, URLName urlname) { + this(session, urlname, "smtp", false); + } + + /** + * Constructor used by this class and by SMTPSSLTransport subclass. + * + * @param session the Session + * @param urlname the URLName of this transport + * @param name the protocol name of this transport + * @param isSSL use SSL to connect? + */ + protected SMTPTransport(Session session, URLName urlname, + String name, boolean isSSL) { + super(session, urlname); + Properties props = session.getProperties(); + + logger = new MailLogger(this.getClass(), "DEBUG SMTP", + session.getDebug(), session.getDebugOut()); + traceLogger = logger.getSubLogger("protocol", null); + noauthdebug = !PropUtil.getBooleanProperty(props, + "mail.debug.auth", false); + debugusername = PropUtil.getBooleanProperty(props, + "mail.debug.auth.username", true); + debugpassword = PropUtil.getBooleanProperty(props, + "mail.debug.auth.password", false); + if (urlname != null) + name = urlname.getProtocol(); + this.name = name; + if (!isSSL) + isSSL = PropUtil.getBooleanProperty(props, + "mail." + name + ".ssl.enable", false); + if (isSSL) + this.defaultPort = 465; + else + this.defaultPort = 25; + this.isSSL = isSSL; + + // setting mail.smtp.quitwait to false causes us to not wait for the + // response from the QUIT command + quitWait = PropUtil.getBooleanProperty(props, + "mail." + name + ".quitwait", true); + + // setting mail.smtp.quitonsessionreject to false causes us to directly + // close the socket without sending a QUIT command + quitOnSessionReject = PropUtil.getBooleanProperty(props, + "mail." + name + ".quitonsessionreject", false); + + // mail.smtp.reportsuccess causes us to throw an exception on success + reportSuccess = PropUtil.getBooleanProperty(props, + "mail." + name + ".reportsuccess", false); + + // mail.smtp.starttls.enable enables use of STARTTLS command + useStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.enable", false); + + // mail.smtp.starttls.required requires use of STARTTLS command + requireStartTLS = PropUtil.getBooleanProperty(props, + "mail." + name + ".starttls.required", false); + + // mail.smtp.userset causes us to use RSET instead of NOOP + // for isConnected + useRset = PropUtil.getBooleanProperty(props, + "mail." + name + ".userset", false); + + // mail.smtp.noop.strict requires 250 response to indicate success + noopStrict = PropUtil.getBooleanProperty(props, + "mail." + name + ".noop.strict", true); + + // check if SASL is enabled + enableSASL = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.enable", false); + if (enableSASL) + logger.config("enable SASL"); + useCanonicalHostName = PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.usecanonicalhostname", false); + if (useCanonicalHostName) + logger.config("use canonical host name"); + + allowutf8 = PropUtil.getBooleanProperty(props, + "mail.mime.allowutf8", false); + if (allowutf8) + logger.config("allow UTF-8"); + + chunkSize = PropUtil.getIntProperty(props, + "mail." + name + ".chunksize", -1); + if (chunkSize > 0 && logger.isLoggable(Level.CONFIG)) + logger.config("chunk size " + chunkSize); + + // created here, because they're inner classes that reference "this" + Authenticator[] a = new Authenticator[] { + new LoginAuthenticator(), + new PlainAuthenticator(), + new DigestMD5Authenticator(), + new NtlmAuthenticator(), + new OAuth2Authenticator() + }; + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < a.length; i++) { + authenticators.put(a[i].getMechanism(), a[i]); + sb.append(a[i].getMechanism()).append(' '); + } + defaultAuthenticationMechanisms = sb.toString(); + } + + /** + * Get the name of the local host, for use in the EHLO and HELO commands. + * The property mail.smtp.localhost overrides mail.smtp.localaddress, + * which overrides what InetAddress would tell us. + * + * @return the local host name + */ + public synchronized String getLocalHost() { + // get our hostname and cache it for future use + if (localHostName == null || localHostName.length() <= 0) + localHostName = + session.getProperty("mail." + name + ".localhost"); + if (localHostName == null || localHostName.length() <= 0) + localHostName = + session.getProperty("mail." + name + ".localaddress"); + try { + if (localHostName == null || localHostName.length() <= 0) { + InetAddress localHost = InetAddress.getLocalHost(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } catch (UnknownHostException uhex) { + } + + // last chance, try to get our address from our socket + if (localHostName == null || localHostName.length() <= 0) { + if (serverSocket != null && serverSocket.isBound()) { + InetAddress localHost = serverSocket.getLocalAddress(); + localHostName = localHost.getCanonicalHostName(); + // if we can't get our name, use local address literal + if (localHostName == null) + // XXX - not correct for IPv6 + localHostName = "[" + localHost.getHostAddress() + "]"; + } + } + return localHostName; + } + + /** + * Set the name of the local host, for use in the EHLO and HELO commands. + * + * @param localhost the local host name + * @since JavaMail 1.3.1 + */ + public synchronized void setLocalHost(String localhost) { + localHostName = localhost; + } + + /** + * Start the SMTP protocol on the given socket, which was already + * connected by the caller. Useful for implementing the SMTP ATRN + * command (RFC 2645) where an existing connection is used when + * the server reverses roles and becomes the client. + * + * @param socket the already connected socket + * @exception MessagingException for failures + * @since JavaMail 1.3.3 + */ + public synchronized void connect(Socket socket) throws MessagingException { + serverSocket = socket; + super.connect(); + } + + /** + * Gets the authorization ID to be used for authentication. + * + * @return the authorization ID to use for authentication. + * + * @since JavaMail 1.4.4 + */ + public synchronized String getAuthorizationId() { + if (authorizationID == UNKNOWN) { + authorizationID = + session.getProperty("mail." + name + ".sasl.authorizationid"); + } + return authorizationID; + } + + /** + * Sets the authorization ID to be used for authentication. + * + * @param authzid the authorization ID to use for + * authentication. + * + * @since JavaMail 1.4.4 + */ + public synchronized void setAuthorizationID(String authzid) { + this.authorizationID = authzid; + } + + /** + * Is SASL authentication enabled? + * + * @return true if SASL authentication is enabled + * + * @since JavaMail 1.4.4 + */ + public synchronized boolean getSASLEnabled() { + return enableSASL; + } + + /** + * Set whether SASL authentication is enabled. + * + * @param enableSASL should we enable SASL authentication? + * + * @since JavaMail 1.4.4 + */ + public synchronized void setSASLEnabled(boolean enableSASL) { + this.enableSASL = enableSASL; + } + + /** + * Gets the SASL realm to be used for DIGEST-MD5 authentication. + * + * @return the name of the realm to use for SASL authentication. + * + * @since JavaMail 1.3.1 + */ + public synchronized String getSASLRealm() { + if (saslRealm == UNKNOWN) { + saslRealm = session.getProperty("mail." + name + ".sasl.realm"); + if (saslRealm == null) // try old name + saslRealm = session.getProperty("mail." + name + ".saslrealm"); + } + return saslRealm; + } + + /** + * Sets the SASL realm to be used for DIGEST-MD5 authentication. + * + * @param saslRealm the name of the realm to use for + * SASL authentication. + * + * @since JavaMail 1.3.1 + */ + public synchronized void setSASLRealm(String saslRealm) { + this.saslRealm = saslRealm; + } + + /** + * Should SASL use the canonical host name? + * + * @return true if SASL should use the canonical host name + * + * @since JavaMail 1.5.2 + */ + public synchronized boolean getUseCanonicalHostName() { + return useCanonicalHostName; + } + + /** + * Set whether SASL should use the canonical host name. + * + * @param useCanonicalHostName should SASL use the canonical host name? + * + * @since JavaMail 1.5.2 + */ + public synchronized void setUseCanonicalHostName( + boolean useCanonicalHostName) { + this.useCanonicalHostName = useCanonicalHostName; + } + + /** + * Get the list of SASL mechanisms to consider if SASL authentication + * is enabled. If the list is empty or null, all available SASL mechanisms + * are considered. + * + * @return the array of SASL mechanisms to consider + * + * @since JavaMail 1.4.4 + */ + public synchronized String[] getSASLMechanisms() { + if (saslMechanisms == UNKNOWN_SA) { + List v = new ArrayList<>(5); + String s = session.getProperty("mail." + name + ".sasl.mechanisms"); + if (s != null && s.length() > 0) { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL mechanisms allowed: " + s); + StringTokenizer st = new StringTokenizer(s, " ,"); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + if (m.length() > 0) + v.add(m); + } + } + saslMechanisms = new String[v.size()]; + v.toArray(saslMechanisms); + } + if (saslMechanisms == null) + return null; + return saslMechanisms.clone(); + } + + /** + * Set the list of SASL mechanisms to consider if SASL authentication + * is enabled. If the list is empty or null, all available SASL mechanisms + * are considered. + * + * @param mechanisms the array of SASL mechanisms to consider + * + * @since JavaMail 1.4.4 + */ + public synchronized void setSASLMechanisms(String[] mechanisms) { + if (mechanisms != null) + mechanisms = mechanisms.clone(); + this.saslMechanisms = mechanisms; + } + + /** + * Gets the NTLM domain to be used for NTLM authentication. + * + * @return the name of the domain to use for NTLM authentication. + * + * @since JavaMail 1.4.3 + */ + public synchronized String getNTLMDomain() { + if (ntlmDomain == UNKNOWN) { + ntlmDomain = + session.getProperty("mail." + name + ".auth.ntlm.domain"); + } + return ntlmDomain; + } + + /** + * Sets the NTLM domain to be used for NTLM authentication. + * + * @param ntlmDomain the name of the domain to use for + * NTLM authentication. + * + * @since JavaMail 1.4.3 + */ + public synchronized void setNTLMDomain(String ntlmDomain) { + this.ntlmDomain = ntlmDomain; + } + + /** + * Should we report even successful sends by throwing an exception? + * If so, a SendFailedException will always be thrown and + * an {@link com.sun.mail.smtp.SMTPAddressSucceededException + * SMTPAddressSucceededException} will be included in the exception + * chain for each successful address, along with the usual + * {@link com.sun.mail.smtp.SMTPAddressFailedException + * SMTPAddressFailedException} for each unsuccessful address. + * + * @return true if an exception will be thrown on successful sends. + * + * @since JavaMail 1.3.2 + */ + public synchronized boolean getReportSuccess() { + return reportSuccess; + } + + /** + * Set whether successful sends should be reported by throwing + * an exception. + * + * @param reportSuccess should we throw an exception on success? + * + * @since JavaMail 1.3.2 + */ + public synchronized void setReportSuccess(boolean reportSuccess) { + this.reportSuccess = reportSuccess; + } + + /** + * Should we use the STARTTLS command to secure the connection + * if the server supports it? + * + * @return true if the STARTTLS command will be used + * + * @since JavaMail 1.3.2 + */ + public synchronized boolean getStartTLS() { + return useStartTLS; + } + + /** + * Set whether the STARTTLS command should be used. + * + * @param useStartTLS should we use the STARTTLS command? + * + * @since JavaMail 1.3.2 + */ + public synchronized void setStartTLS(boolean useStartTLS) { + this.useStartTLS = useStartTLS; + } + + /** + * Should we require the STARTTLS command to secure the connection? + * + * @return true if the STARTTLS command will be required + * + * @since JavaMail 1.4.2 + */ + public synchronized boolean getRequireStartTLS() { + return requireStartTLS; + } + + /** + * Set whether the STARTTLS command should be required. + * + * @param requireStartTLS should we require the STARTTLS command? + * + * @since JavaMail 1.4.2 + */ + public synchronized void setRequireStartTLS(boolean requireStartTLS) { + this.requireStartTLS = requireStartTLS; + } + + /** + * Is this Transport using SSL to connect to the server? + * + * @return true if using SSL + * @since JavaMail 1.4.6 + */ + public synchronized boolean isSSL() { + return serverSocket instanceof SSLSocket; + } + + /** + * Should we use the RSET command instead of the NOOP command + * in the @{link #isConnected isConnected} method? + * + * @return true if RSET will be used + * + * @since JavaMail 1.4 + */ + public synchronized boolean getUseRset() { + return useRset; + } + + /** + * Set whether the RSET command should be used instead of the + * NOOP command in the @{link #isConnected isConnected} method. + * + * @param useRset should we use the RSET command? + * + * @since JavaMail 1.4 + */ + public synchronized void setUseRset(boolean useRset) { + this.useRset = useRset; + } + + /** + * Is the NOOP command required to return a response code + * of 250 to indicate success? + * + * @return true if NOOP must return 250 + * + * @since JavaMail 1.4.3 + */ + public synchronized boolean getNoopStrict() { + return noopStrict; + } + + /** + * Set whether the NOOP command is required to return a response code + * of 250 to indicate success. + * + * @param noopStrict is NOOP required to return 250? + * + * @since JavaMail 1.4.3 + */ + public synchronized void setNoopStrict(boolean noopStrict) { + this.noopStrict = noopStrict; + } + + /** + * Return the last response we got from the server. + * A failed send is often followed by an RSET command, + * but the response from the RSET command is not saved. + * Instead, this returns the response from the command + * before the RSET command. + * + * @return last response from server + * + * @since JavaMail 1.3.2 + */ + public synchronized String getLastServerResponse() { + return lastServerResponse; + } + + /** + * Return the return code from the last response we got from the server. + * + * @return return code from last response from server + * + * @since JavaMail 1.4.1 + */ + public synchronized int getLastReturnCode() { + return lastReturnCode; + } + + /** + * Performs the actual protocol-specific connection attempt. + * Will attempt to connect to "localhost" if the host was null.

+ * + * Unless mail.smtp.ehlo is set to false, we'll try to identify + * ourselves using the ESMTP command EHLO. + * + * If mail.smtp.auth is set to true, we insist on having a username + * and password, and will try to authenticate ourselves if the server + * supports the AUTH extension (RFC 2554). + * + * @param host the name of the host to connect to + * @param port the port to use (-1 means use default port) + * @param user the name of the user to login as + * @param password the user's password + * @return true if connection successful, false if authentication failed + * @exception MessagingException for non-authentication failures + */ + @Override + protected synchronized boolean protocolConnect(String host, int port, + String user, String password) + throws MessagingException { + Properties props = session.getProperties(); + + // setting mail.smtp.auth to true enables attempts to use AUTH + boolean useAuth = PropUtil.getBooleanProperty(props, + "mail." + name + ".auth", false); + + /* + * If mail.smtp.auth is set, make sure we have a valid username + * and password, even if we might not end up using it (e.g., + * because the server doesn't support ESMTP or doesn't support + * the AUTH extension). + */ + if (useAuth && (user == null || password == null)) { + if (logger.isLoggable(Level.FINE)) { + logger.fine("need username and password for authentication"); + logger.fine("protocolConnect returning false" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + } + return false; + } + + // setting mail.smtp.ehlo to false disables attempts to use EHLO + boolean useEhlo = PropUtil.getBooleanProperty(props, + "mail." + name + ".ehlo", true); + if (logger.isLoggable(Level.FINE)) + logger.fine("useEhlo " + useEhlo + ", useAuth " + useAuth); + + /* + * If port is not specified, set it to value of mail.smtp.port + * property if it exists, otherwise default to 25. + */ + if (port == -1) + port = PropUtil.getIntProperty(props, + "mail." + name + ".port", -1); + if (port == -1) + port = defaultPort; + + if (host == null || host.length() == 0) + host = "localhost"; + + /* + * If anything goes wrong, we need to be sure + * to close the connection. + */ + boolean connected = false; + try { + + if (serverSocket != null) + openServer(); // only happens from connect(socket) + else + openServer(host, port); + + boolean succeed = false; + if (useEhlo) + succeed = ehlo(getLocalHost()); + if (!succeed) + helo(getLocalHost()); + + if (useStartTLS || requireStartTLS) { + if (serverSocket instanceof SSLSocket) { + logger.fine("STARTTLS requested but already using SSL"); + } else if (supportsExtension("STARTTLS")) { + startTLS(); + /* + * Have to issue another EHLO to update list of extensions + * supported, especially authentication mechanisms. + * Don't know if this could ever fail, but we ignore + * failure. + */ + ehlo(getLocalHost()); + } else if (requireStartTLS) { + logger.fine("STARTTLS required but not supported"); + throw new MessagingException( + "STARTTLS is required but " + + "host does not support STARTTLS"); + } + } + + if (allowutf8 && !supportsExtension("SMTPUTF8")) + logger.log(Level.INFO, "mail.mime.allowutf8 set " + + "but server doesn't advertise SMTPUTF8 support"); + + if ((useAuth || (user != null && password != null)) && + (supportsExtension("AUTH") || + supportsExtension("AUTH=LOGIN"))) { + if (logger.isLoggable(Level.FINE)) + logger.fine("protocolConnect login" + + ", host=" + host + + ", user=" + traceUser(user) + + ", password=" + tracePassword(password)); + connected = authenticate(user, password); + return connected; + } + + // we connected correctly + connected = true; + return true; + + } finally { + // if we didn't connect successfully, + // make sure the connection is closed + if (!connected) { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } + } + } + } + + /** + * Authenticate to the server. + */ + private boolean authenticate(String user, String passwd) + throws MessagingException { + // setting mail.smtp.auth.mechanisms controls which mechanisms will + // be used, and in what order they'll be considered. only the first + // match is used. + String mechs = session.getProperty("mail." + name + ".auth.mechanisms"); + if (mechs == null) + mechs = defaultAuthenticationMechanisms; + + String authzid = getAuthorizationId(); + if (authzid == null) + authzid = user; + if (enableSASL) { + logger.fine("Authenticate with SASL"); + try { + if (sasllogin(getSASLMechanisms(), getSASLRealm(), authzid, + user, passwd)) { + return true; // success + } else { + logger.fine("SASL authentication failed"); + return false; + } + } catch (UnsupportedOperationException ex) { + logger.log(Level.FINE, "SASL support failed", ex); + // if the SASL support fails, fall back to non-SASL + } + } + + if (logger.isLoggable(Level.FINE)) + logger.fine("Attempt to authenticate using mechanisms: " + mechs); + + /* + * Loop through the list of mechanisms supplied by the user + * (or defaulted) and try each in turn. If the server supports + * the mechanism and we have an authenticator for the mechanism, + * and it hasn't been disabled, use it. + */ + StringTokenizer st = new StringTokenizer(mechs); + while (st.hasMoreTokens()) { + String m = st.nextToken(); + m = m.toUpperCase(Locale.ENGLISH); + Authenticator a = authenticators.get(m); + if (a == null) { + logger.log(Level.FINE, "no authenticator for mechanism {0}", m); + continue; + } + + if (!supportsAuthentication(m)) { + logger.log(Level.FINE, "mechanism {0} not supported by server", + m); + continue; + } + + /* + * If using the default mechanisms, check if this one is disabled. + */ + if (mechs == defaultAuthenticationMechanisms) { + String dprop = "mail." + name + ".auth." + + m.toLowerCase(Locale.ENGLISH) + ".disable"; + boolean disabled = PropUtil.getBooleanProperty( + session.getProperties(), + dprop, !a.enabled()); + if (disabled) { + if (logger.isLoggable(Level.FINE)) + logger.fine("mechanism " + m + + " disabled by property: " + dprop); + continue; + } + } + + // only the first supported and enabled mechanism is used + logger.log(Level.FINE, "Using mechanism {0}", m); + return a.authenticate(host, authzid, user, passwd); + } + + // if no authentication mechanism found, fail + throw new AuthenticationFailedException( + "No authentication mechanisms supported by both server and client"); + } + + /** + * Abstract base class for SMTP authentication mechanism implementations. + */ + private abstract class Authenticator { + protected int resp; // the response code, used by subclasses + private final String mech; // the mechanism name, set in the constructor + private final boolean enabled; // is this mechanism enabled by default? + + Authenticator(String mech) { + this(mech, true); + } + + Authenticator(String mech, boolean enabled) { + this.mech = mech.toUpperCase(Locale.ENGLISH); + this.enabled = enabled; + } + + String getMechanism() { + return mech; + } + + boolean enabled() { + return enabled; + } + + /** + * Start the authentication handshake by issuing the AUTH command. + * Delegate to the doAuth method to do the mechanism-specific + * part of the handshake. + */ + boolean authenticate(String host, String authzid, + String user, String passwd) throws MessagingException { + Throwable thrown = null; + try { + // use "initial response" capability, if supported + String ir = getInitialResponse(host, authzid, user, passwd); + if (noauthdebug && isTracing()) { + logger.fine("AUTH " + mech + " command trace suppressed"); + suspendTracing(); + } + if (ir != null) + resp = simpleCommand("AUTH " + mech + " " + + (ir.length() == 0 ? "=" : ir)); + else + resp = simpleCommand("AUTH " + mech); + + /* + * A 530 response indicates that the server wants us to + * issue a STARTTLS command first. Do that and try again. + */ + if (resp == 530) { + startTLS(); + if (ir != null) + resp = simpleCommand("AUTH " + mech + " " + ir); + else + resp = simpleCommand("AUTH " + mech); + } + if (resp == 334) + doAuth(host, authzid, user, passwd); + } catch (IOException ex) { // should never happen, ignore + logger.log(Level.FINE, "AUTH " + mech + " failed", ex); + } catch (Throwable t) { // crypto can't be initialized? + logger.log(Level.FINE, "AUTH " + mech + " failed", t); + thrown = t; + } finally { + if (noauthdebug && isTracing()) + logger.fine("AUTH " + mech + " " + + (resp == 235 ? "succeeded" : "failed")); + resumeTracing(); + if (resp != 235) { + closeConnection(); + if (thrown != null) { + if (thrown instanceof Error) + throw (Error)thrown; + if (thrown instanceof Exception) + throw new AuthenticationFailedException( + getLastServerResponse(), + (Exception)thrown); + assert false : "unknown Throwable"; // can't happen + } + throw new AuthenticationFailedException( + getLastServerResponse()); + } + } + return true; + } + + /** + * Provide the initial response to use in the AUTH command, + * or null if not supported. Subclasses that support the + * initial response capability will override this method. + */ + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + return null; + } + + abstract void doAuth(String host, String authzid, String user, + String passwd) throws MessagingException, IOException; + } + + /** + * Perform the authentication handshake for LOGIN authentication. + */ + private class LoginAuthenticator extends Authenticator { + LoginAuthenticator() { + super("LOGIN"); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + // send username + resp = simpleCommand(BASE64EncoderStream.encode( + user.getBytes(StandardCharsets.UTF_8))); + if (resp == 334) { + // send passwd + resp = simpleCommand(BASE64EncoderStream.encode( + passwd.getBytes(StandardCharsets.UTF_8))); + } + } + } + + /** + * Perform the authentication handshake for PLAIN authentication. + */ + private class PlainAuthenticator extends Authenticator { + PlainAuthenticator() { + super("PLAIN"); + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + // return "authziduserpasswd" + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + OutputStream b64os = + new BASE64EncoderStream(bos, Integer.MAX_VALUE); + if (authzid != null) + b64os.write(authzid.getBytes(StandardCharsets.UTF_8)); + b64os.write(0); + b64os.write(user.getBytes(StandardCharsets.UTF_8)); + b64os.write(0); + b64os.write(passwd.getBytes(StandardCharsets.UTF_8)); + b64os.flush(); // complete the encoding + + return ASCIIUtility.toString(bos.toByteArray()); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + // should never get here + throw new AuthenticationFailedException("PLAIN asked for more"); + } + } + + /** + * Perform the authentication handshake for DIGEST-MD5 authentication. + */ + private class DigestMD5Authenticator extends Authenticator { + private DigestMD5 md5support; // only create if needed + + DigestMD5Authenticator() { + super("DIGEST-MD5"); + } + + private synchronized DigestMD5 getMD5() { + if (md5support == null) + md5support = new DigestMD5(logger); + return md5support; + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + DigestMD5 md5 = getMD5(); + assert md5 != null; + + byte[] b = md5.authClient(host, user, passwd, getSASLRealm(), + getLastServerResponse()); + resp = simpleCommand(b); + if (resp == 334) { // client authenticated by server + if (!md5.authServer(getLastServerResponse())) { + // server NOT authenticated by client !!! + resp = -1; + } else { + // send null response + resp = simpleCommand(new byte[0]); + } + } + } + } + + /** + * Perform the authentication handshake for NTLM authentication. + */ + private class NtlmAuthenticator extends Authenticator { + private Ntlm ntlm; + + NtlmAuthenticator() { + super("NTLM"); + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + ntlm = new Ntlm(getNTLMDomain(), getLocalHost(), + user, passwd, logger); + + int flags = PropUtil.getIntProperty( + session.getProperties(), + "mail." + name + ".auth.ntlm.flags", 0); + boolean v2 = PropUtil.getBooleanProperty( + session.getProperties(), + "mail." + name + ".auth.ntlm.v2", true); + + String type1 = ntlm.generateType1Msg(flags, v2); + return type1; + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + assert ntlm != null; + String type3 = ntlm.generateType3Msg( + getLastServerResponse().substring(4).trim()); + + resp = simpleCommand(type3); + } + } + + /** + * Perform the authentication handshake for XOAUTH2 authentication. + */ + private class OAuth2Authenticator extends Authenticator { + + OAuth2Authenticator() { + super("XOAUTH2", false); // disabled by default + } + + @Override + String getInitialResponse(String host, String authzid, String user, + String passwd) throws MessagingException, IOException { + String resp = "user=" + user + "\001auth=Bearer " + + passwd + "\001\001"; + byte[] b = BASE64EncoderStream.encode( + resp.getBytes(StandardCharsets.UTF_8)); + return ASCIIUtility.toString(b); + } + + @Override + void doAuth(String host, String authzid, String user, String passwd) + throws MessagingException, IOException { + // should never get here + throw new AuthenticationFailedException("OAUTH2 asked for more"); + } + } + + /** + * SASL-based login. + * + * @param allowed the allowed SASL mechanisms + * @param realm the SASL realm + * @param authzid the authorization ID + * @param u the user name for authentication + * @param p the password for authentication + * @return true for success + * @exception MessagingException for failures + */ + private boolean sasllogin(String[] allowed, String realm, String authzid, + String u, String p) throws MessagingException { + String serviceHost; + if (useCanonicalHostName) + serviceHost = serverSocket.getInetAddress().getCanonicalHostName(); + else + serviceHost = host; + if (saslAuthenticator == null) { + try { + Class sac = Class.forName( + "com.sun.mail.smtp.SMTPSaslAuthenticator"); + Constructor c = sac.getConstructor(new Class[] { + SMTPTransport.class, + String.class, + Properties.class, + MailLogger.class, + String.class + }); + saslAuthenticator = (SaslAuthenticator)c.newInstance( + new Object[] { + this, + name, + session.getProperties(), + logger, + serviceHost + }); + } catch (Exception ex) { + logger.log(Level.FINE, "Can't load SASL authenticator", ex); + // probably because we're running on a system without SASL + return false; // not authenticated, try without SASL + } + } + + // were any allowed mechanisms specified? + List v; + if (allowed != null && allowed.length > 0) { + // remove anything not supported by the server + v = new ArrayList<>(allowed.length); + for (int i = 0; i < allowed.length; i++) + if (supportsAuthentication(allowed[i])) // XXX - case must match + v.add(allowed[i]); + } else { + // everything is allowed + v = new ArrayList<>(); + if (extMap != null) { + String a = extMap.get("AUTH"); + if (a != null) { + StringTokenizer st = new StringTokenizer(a); + while (st.hasMoreTokens()) + v.add(st.nextToken()); + } + } + } + String[] mechs = v.toArray(new String[v.size()]); + try { + if (noauthdebug && isTracing()) { + logger.fine("SASL AUTH command trace suppressed"); + suspendTracing(); + } + return saslAuthenticator.authenticate(mechs, realm, authzid, u, p); + } finally { + resumeTracing(); + } + } + + /** + * Send the Message to the specified list of addresses.

+ * + * If all the addresses succeed the SMTP check + * using the RCPT TO: command, we attempt to send the message. + * A TransportEvent of type MESSAGE_DELIVERED is fired indicating the + * successful submission of a message to the SMTP host.

+ * + * If some of the addresses fail the SMTP check, + * and the mail.smtp.sendpartial property is not set, + * sending is aborted. The TransportEvent of type MESSAGE_NOT_DELIVERED + * is fired containing the valid and invalid addresses. The + * SendFailedException is also thrown.

+ * + * If some of the addresses fail the SMTP check, + * and the mail.smtp.sendpartial property is set to true, + * the message is sent. The TransportEvent of type + * MESSAGE_PARTIALLY_DELIVERED + * is fired containing the valid and invalid addresses. The + * SMTPSendFailedException is also thrown.

+ * + * MessagingException is thrown if the message can't write out + * an RFC822-compliant stream using its writeTo method.

+ * + * @param message The MimeMessage to be sent + * @param addresses List of addresses to send this message to + * @see javax.mail.event.TransportEvent + * @exception SMTPSendFailedException if the send failed because of + * an SMTP command error + * @exception SendFailedException if the send failed because of + * invalid addresses. + * @exception MessagingException if the connection is dead + * or not in the connected state or if the message is + * not a MimeMessage. + */ + @Override + public synchronized void sendMessage(Message message, Address[] addresses) + throws MessagingException, SendFailedException { + + sendMessageStart(message != null ? message.getSubject() : ""); + checkConnected(); + + // check if the message is a valid MIME/RFC822 message and that + // it has all valid InternetAddresses; fail if not + if (!(message instanceof MimeMessage)) { + logger.fine("Can only send RFC822 msgs"); + throw new MessagingException("SMTP can only send RFC822 messages"); + } + if (addresses == null || addresses.length == 0) { + throw new SendFailedException("No recipient addresses"); + } + for (int i = 0; i < addresses.length; i++) { + if (!(addresses[i] instanceof InternetAddress)) { + throw new MessagingException(addresses[i] + + " is not an InternetAddress"); + } + } + + this.message = (MimeMessage)message; + this.addresses = addresses; + validUnsentAddr = addresses; // until we know better + expandGroups(); + + boolean use8bit = false; + if (message instanceof SMTPMessage) + use8bit = ((SMTPMessage)message).getAllow8bitMIME(); + if (!use8bit) + use8bit = PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".allow8bitmime", false); + if (logger.isLoggable(Level.FINE)) + logger.fine("use8bit " + use8bit); + if (use8bit && supportsExtension("8BITMIME")) { + if (convertTo8Bit(this.message)) { + // in case we made any changes, save those changes + // XXX - this will change the Message-ID + try { + this.message.saveChanges(); + } catch (MessagingException mex) { + // ignore it + } + } + } + + try { + mailFrom(); + rcptTo(); + if (chunkSize > 0 && supportsExtension("CHUNKING")) { + /* + * Use BDAT to send the data in chunks. + * Note that even though the BDAT command is able to send + * messages that contain binary data, we can't use it to + * do that because a) we still need to canonicalize the + * line terminators for text data, which we can't tell apart + * from the message content, and b) the message content is + * encoded before we even know that we can use BDAT. + */ + this.message.writeTo(bdat(), ignoreList); + finishBdat(); + } else { + this.message.writeTo(data(), ignoreList); + finishData(); + } + if (sendPartiallyFailed) { + // throw the exception, + // fire TransportEvent.MESSAGE_PARTIALLY_DELIVERED event + logger.fine("Sending partially failed " + + "because of invalid destination addresses"); + notifyTransportListeners( + TransportEvent.MESSAGE_PARTIALLY_DELIVERED, + validSentAddr, validUnsentAddr, invalidAddr, + this.message); + + throw new SMTPSendFailedException(".", lastReturnCode, + lastServerResponse, exception, + validSentAddr, validUnsentAddr, invalidAddr); + } + logger.fine("message successfully delivered to mail server"); + notifyTransportListeners(TransportEvent.MESSAGE_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + } catch (MessagingException mex) { + logger.log(Level.FINE, "MessagingException while sending", mex); + // the MessagingException might be wrapping an IOException + if (mex.getNextException() instanceof IOException) { + // if we catch an IOException, it means that we want + // to drop the connection so that the message isn't sent + logger.fine("nested IOException, closing"); + try { + closeConnection(); + } catch (MessagingException cex) { /* ignore it */ } + } + addressesFailed(); + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + + throw mex; + } catch (IOException ex) { + logger.log(Level.FINE, "IOException while sending, closing", ex); + // if we catch an IOException, it means that we want + // to drop the connection so that the message isn't sent + try { + closeConnection(); + } catch (MessagingException mex) { /* ignore it */ } + addressesFailed(); + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + + throw new MessagingException("IOException while sending message", + ex); + } finally { + // no reason to keep this data around + validSentAddr = validUnsentAddr = invalidAddr = null; + this.addresses = null; + this.message = null; + this.exception = null; + sendPartiallyFailed = false; + notificationDone = false; // reset for next send + } + sendMessageEnd(); + } + + /** + * The send failed, fix the address arrays to report the failure correctly. + */ + private void addressesFailed() { + if (validSentAddr != null) { + if (validUnsentAddr != null) { + Address newa[] = + new Address[validSentAddr.length + validUnsentAddr.length]; + System.arraycopy(validSentAddr, 0, + newa, 0, validSentAddr.length); + System.arraycopy(validUnsentAddr, 0, + newa, validSentAddr.length, validUnsentAddr.length); + validSentAddr = null; + validUnsentAddr = newa; + } else { + validUnsentAddr = validSentAddr; + validSentAddr = null; + } + } + } + + /** + * Close the Transport and terminate the connection to the server. + */ + @Override + public synchronized void close() throws MessagingException { + if (!super.isConnected()) // Already closed. + return; + try { + if (serverSocket != null) { + sendCommand("QUIT"); + if (quitWait) { + int resp = readServerResponse(); + if (resp != 221 && resp != -1 && + logger.isLoggable(Level.FINE)) + logger.fine("QUIT failed with " + resp); + } + } + } finally { + closeConnection(); + } + } + + private void closeConnection() throws MessagingException { + try { + if (serverSocket != null) + serverSocket.close(); + } catch (IOException ioex) { // shouldn't happen + throw new MessagingException("Server Close Failed", ioex); + } finally { + serverSocket = null; + serverOutput = null; + serverInput = null; + lineInputStream = null; + if (super.isConnected()) // only notify if already connected + super.close(); + } + } + + /** + * Check whether the transport is connected. Override superclass + * method, to actually ping our server connection. + */ + @Override + public synchronized boolean isConnected() { + if (!super.isConnected()) + // if we haven't been connected at all, don't bother with NOOP + return false; + + try { + // sendmail may respond slowly to NOOP after many requests + // so if mail.smtp.userset is set we use RSET instead of NOOP. + if (useRset) + sendCommand("RSET"); + else + sendCommand("NOOP"); + int resp = readServerResponse(); + + /* + * NOOP should return 250 on success, however, SIMS 3.2 returns + * 200, so we work around it. + * + * Hotmail didn't used to implement the NOOP command at all so + * assume any kind of response means we're still connected. + * That is, any response except 421, which means the server + * is shutting down the connection. + * + * Some versions of Exchange return 451 instead of 421 when + * timing out a connection. + * + * Argh! + * + * If mail.smtp.noop.strict is set to false, be tolerant of + * servers that return the wrong response code for success. + */ + if (resp >= 0 && (noopStrict ? resp == 250 : resp != 421)) { + return true; + } else { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } + return false; + } + } catch (Exception ex) { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } + return false; + } + } + + /** + * Notify all TransportListeners. Keep track of whether notification + * has been done so as to only notify once per send. + * + * @since JavaMail 1.4.2 + */ + @Override + protected void notifyTransportListeners(int type, Address[] validSent, + Address[] validUnsent, + Address[] invalid, Message msg) { + + if (!notificationDone) { + super.notifyTransportListeners(type, validSent, validUnsent, + invalid, msg); + notificationDone = true; + } + } + + /** + * Expand any group addresses. + */ + private void expandGroups() { + List

groups = null; + for (int i = 0; i < addresses.length; i++) { + InternetAddress a = (InternetAddress)addresses[i]; + if (a.isGroup()) { + if (groups == null) { + // first group, catch up with where we are + groups = new ArrayList<>(); + for (int k = 0; k < i; k++) + groups.add(addresses[k]); + } + // parse it and add each individual address + try { + InternetAddress[] ia = a.getGroup(true); + if (ia != null) { + for (int j = 0; j < ia.length; j++) + groups.add(ia[j]); + } else + groups.add(a); + } catch (ParseException pex) { + // parse failed, add the whole thing + groups.add(a); + } + } else { + // if we've started accumulating a list, add this to it + if (groups != null) + groups.add(a); + } + } + + // if we have a new list, convert it back to an array + if (groups != null) { + InternetAddress[] newa = new InternetAddress[groups.size()]; + groups.toArray(newa); + addresses = newa; + } + } + + /** + * If the Part is a text part and has a Content-Transfer-Encoding + * of "quoted-printable" or "base64", and it obeys the rules for + * "8bit" encoding, change the encoding to "8bit". If the part is + * a multipart, recursively process all its parts. + * + * @return true if any changes were made + * + * XXX - This is really quite a hack. + */ + private boolean convertTo8Bit(MimePart part) { + boolean changed = false; + try { + if (part.isMimeType("text/*")) { + String enc = part.getEncoding(); + if (enc != null && (enc.equalsIgnoreCase("quoted-printable") || + enc.equalsIgnoreCase("base64"))) { + InputStream is = null; + try { + is = part.getInputStream(); + if (is8Bit(is)) { + /* + * If the message was created using an InputStream + * then we have to extract the content as an object + * and set it back as an object so that the content + * will be re-encoded. + * + * If the message was not created using an + * InputStream, the following should have no effect. + */ + part.setContent(part.getContent(), + part.getContentType()); + part.setHeader("Content-Transfer-Encoding", "8bit"); + changed = true; + } + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException ex2) { + // ignore it + } + } + } + } + } else if (part.isMimeType("multipart/*")) { + MimeMultipart mp = (MimeMultipart)part.getContent(); + int count = mp.getCount(); + for (int i = 0; i < count; i++) { + if (convertTo8Bit((MimePart)mp.getBodyPart(i))) + changed = true; + } + } + } catch (IOException ioex) { + // any exception causes us to give up + } catch (MessagingException mex) { + // any exception causes us to give up + } + return changed; + } + + /** + * Check whether the data in the given InputStream follows the + * rules for 8bit text. Lines have to be 998 characters or less + * and no NULs are allowed. CR and LF must occur in pairs but we + * don't check that because we assume this is text and we convert + * all CR/LF combinations into canonical CRLF later. + */ + private boolean is8Bit(InputStream is) { + int b; + int linelen = 0; + boolean need8bit = false; + try { + while ((b = is.read()) >= 0) { + b &= 0xff; + if (b == '\r' || b == '\n') + linelen = 0; + else if (b == 0) + return false; + else { + linelen++; + if (linelen > 998) // 1000 - CRLF + return false; + } + if (b > 0x7f) + need8bit = true; + } + } catch (IOException ex) { + return false; + } + if (need8bit) + logger.fine("found an 8bit part"); + return need8bit; + } + + @Override + protected void finalize() throws Throwable { + try { + closeConnection(); + } catch (MessagingException mex) { + // ignore it + } finally { + super.finalize(); + } + } + + ///////////////////// smtp stuff /////////////////////// + private BufferedInputStream serverInput; + private LineInputStream lineInputStream; + private OutputStream serverOutput; + private Socket serverSocket; + private TraceInputStream traceInput; + private TraceOutputStream traceOutput; + + /////// smtp protocol ////// + + /** + * Issue the HELO command. + * + * @param domain our domain + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void helo(String domain) throws MessagingException { + if (domain != null) + issueCommand("HELO " + domain, 250); + else + issueCommand("HELO", 250); + } + + /** + * Issue the EHLO command. + * Collect the returned list of service extensions. + * + * @param domain our domain + * @return true if command succeeds + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected boolean ehlo(String domain) throws MessagingException { + String cmd; + if (domain != null) + cmd = "EHLO " + domain; + else + cmd = "EHLO"; + sendCommand(cmd); + int resp = readServerResponse(); + if (resp == 250) { + // extract the supported service extensions + BufferedReader rd = + new BufferedReader(new StringReader(lastServerResponse)); + String line; + extMap = new Hashtable<>(); + try { + boolean first = true; + while ((line = rd.readLine()) != null) { + if (first) { // skip first line which is the greeting + first = false; + continue; + } + if (line.length() < 5) + continue; // shouldn't happen + line = line.substring(4); // skip response code + int i = line.indexOf(' '); + String arg = ""; + if (i > 0) { + arg = line.substring(i + 1); + line = line.substring(0, i); + } + if (logger.isLoggable(Level.FINE)) + logger.fine("Found extension \"" + + line + "\", arg \"" + arg + "\""); + extMap.put(line.toUpperCase(Locale.ENGLISH), arg); + } + } catch (IOException ex) { } // can't happen + } + return resp == 250; + } + + /** + * Issue the MAIL FROM: command to start sending a message.

+ * + * Gets the sender's address in the following order: + *

    + *
  1. SMTPMessage.getEnvelopeFrom()
  2. + *
  3. mail.smtp.from property
  4. + *
  5. From: header in the message
  6. + *
  7. System username using the + * InternetAddress.getLocalAddress() method
  8. + *
+ * + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void mailFrom() throws MessagingException { + String from = null; + if (message instanceof SMTPMessage) + from = ((SMTPMessage)message).getEnvelopeFrom(); + if (from == null || from.length() <= 0) + from = session.getProperty("mail." + name + ".from"); + if (from == null || from.length() <= 0) { + Address[] fa; + Address me; + if (message != null && (fa = message.getFrom()) != null && + fa.length > 0) + me = fa[0]; + else + me = InternetAddress.getLocalAddress(session); + + if (me != null) + from = ((InternetAddress)me).getAddress(); + else + throw new MessagingException( + "can't determine local email address"); + } + + String cmd = "MAIL FROM:" + normalizeAddress(from); + + if (allowutf8 && supportsExtension("SMTPUTF8")) + cmd += " SMTPUTF8"; + + // request delivery status notification? + if (supportsExtension("DSN")) { + String ret = null; + if (message instanceof SMTPMessage) + ret = ((SMTPMessage)message).getDSNRet(); + if (ret == null) + ret = session.getProperty("mail." + name + ".dsn.ret"); + // XXX - check for legal syntax? + if (ret != null) + cmd += " RET=" + ret; + } + + /* + * If an RFC 2554 submitter has been specified, and the server + * supports the AUTH extension, include the AUTH= element on + * the MAIL FROM command. + */ + if (supportsExtension("AUTH")) { + String submitter = null; + if (message instanceof SMTPMessage) + submitter = ((SMTPMessage)message).getSubmitter(); + if (submitter == null) + submitter = session.getProperty("mail." + name + ".submitter"); + // XXX - check for legal syntax? + if (submitter != null) { + try { + String s = xtext(submitter, + allowutf8 && supportsExtension("SMTPUTF8")); + cmd += " AUTH=" + s; + } catch (IllegalArgumentException ex) { + if (logger.isLoggable(Level.FINE)) + logger.log(Level.FINE, "ignoring invalid submitter: " + + submitter, ex); + } + } + } + + /* + * Have any extensions to the MAIL command been specified? + */ + String ext = null; + if (message instanceof SMTPMessage) + ext = ((SMTPMessage)message).getMailExtension(); + if (ext == null) + ext = session.getProperty("mail." + name + ".mailextension"); + if (ext != null && ext.length() > 0) + cmd += " " + ext; + + try { + issueSendCommand(cmd, 250); + } catch (SMTPSendFailedException ex) { + int retCode = ex.getReturnCode(); + switch (retCode) { + case 550: case 553: case 503: case 551: case 501: + // given address is invalid + try { + ex.setNextException(new SMTPSenderFailedException( + new InternetAddress(from), cmd, + retCode, ex.getMessage())); + } catch (AddressException aex) { + // oh well... + } + break; + default: + break; + } + throw ex; + } + } + + /** + * Sends each address to the SMTP host using the RCPT TO: + * command and copies the address either into + * the validSentAddr or invalidAddr arrays. + * Sets the sendFailed + * flag to true if any addresses failed. + * + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + /* + * success/failure/error possibilities from the RCPT command + * from rfc821, section 4.3 + * S: 250, 251 + * F: 550, 551, 552, 553, 450, 451, 452 + * E: 500, 501, 503, 421 + * + * and how we map the above error/failure conditions to valid/invalid + * address lists that are reported in the thrown exception: + * invalid addr: 550, 501, 503, 551, 553 + * valid addr: 552 (quota), 450, 451, 452 (quota), 421 (srvr abort) + */ + protected void rcptTo() throws MessagingException { + List valid = new ArrayList<>(); + List validUnsent = new ArrayList<>(); + List invalid = new ArrayList<>(); + int retCode = -1; + MessagingException mex = null; + boolean sendFailed = false; + MessagingException sfex = null; + validSentAddr = validUnsentAddr = invalidAddr = null; + boolean sendPartial = false; + if (message instanceof SMTPMessage) + sendPartial = ((SMTPMessage)message).getSendPartial(); + if (!sendPartial) + sendPartial = PropUtil.getBooleanProperty(session.getProperties(), + "mail." + name + ".sendpartial", false); + if (sendPartial) + logger.fine("sendPartial set"); + + boolean dsn = false; + String notify = null; + if (supportsExtension("DSN")) { + if (message instanceof SMTPMessage) + notify = ((SMTPMessage)message).getDSNNotify(); + if (notify == null) + notify = session.getProperty("mail." + name + ".dsn.notify"); + // XXX - check for legal syntax? + if (notify != null) + dsn = true; + } + + // try the addresses one at a time + for (int i = 0; i < addresses.length; i++) { + + sfex = null; + InternetAddress ia = (InternetAddress)addresses[i]; + String cmd = "RCPT TO:" + normalizeAddress(ia.getAddress()); + if (dsn) + cmd += " NOTIFY=" + notify; + // send the addresses to the SMTP server + sendCommand(cmd); + // check the server's response for address validity + retCode = readServerResponse(); + switch (retCode) { + case 250: case 251: + valid.add(ia); + if (!reportSuccess) + break; + + // user wants exception even when successful, including + // details of the return code + + // create and chain the exception + sfex = new SMTPAddressSucceededException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + + case 550: case 553: case 503: case 551: case 501: + // given address is invalid + if (!sendPartial) + sendFailed = true; + invalid.add(ia); + // create and chain the exception + sfex = new SMTPAddressFailedException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + + case 552: case 450: case 451: case 452: + // given address is valid + if (!sendPartial) + sendFailed = true; + validUnsent.add(ia); + // create and chain the exception + sfex = new SMTPAddressFailedException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + + default: + // handle remaining 4xy & 5xy codes + if (retCode >= 400 && retCode <= 499) { + // assume address is valid, although we don't really know + validUnsent.add(ia); + } else if (retCode >= 500 && retCode <= 599) { + // assume address is invalid, although we don't really know + invalid.add(ia); + } else { + // completely unexpected response, just give up + if (logger.isLoggable(Level.FINE)) + logger.fine("got response code " + retCode + + ", with response: " + lastServerResponse); + String _lsr = lastServerResponse; // else rset will nuke it + int _lrc = lastReturnCode; + if (serverSocket != null) // hasn't already been closed + issueCommand("RSET", -1); + lastServerResponse = _lsr; // restore, for get + lastReturnCode = _lrc; + throw new SMTPAddressFailedException(ia, cmd, retCode, + _lsr); + } + if (!sendPartial) + sendFailed = true; + // create and chain the exception + sfex = new SMTPAddressFailedException(ia, cmd, retCode, + lastServerResponse); + if (mex == null) + mex = sfex; + else + mex.setNextException(sfex); + break; + } + } + + // if we're willing to send to a partial list, and we found no + // valid addresses, that's complete failure + if (sendPartial && valid.size() == 0) + sendFailed = true; + + // copy the lists into appropriate arrays + if (sendFailed) { + // copy invalid addrs + invalidAddr = new Address[invalid.size()]; + invalid.toArray(invalidAddr); + + // copy all valid addresses to validUnsent, since something failed + validUnsentAddr = new Address[valid.size() + validUnsent.size()]; + int i = 0; + for (int j = 0; j < valid.size(); j++) + validUnsentAddr[i++] = (Address)valid.get(j); + for (int j = 0; j < validUnsent.size(); j++) + validUnsentAddr[i++] = (Address)validUnsent.get(j); + } else if (reportSuccess || (sendPartial && + (invalid.size() > 0 || validUnsent.size() > 0))) { + // we'll go on to send the message, but after sending we'll + // throw an exception with this exception nested + sendPartiallyFailed = true; + exception = mex; + + // copy invalid addrs + invalidAddr = new Address[invalid.size()]; + invalid.toArray(invalidAddr); + + // copy valid unsent addresses to validUnsent + validUnsentAddr = new Address[validUnsent.size()]; + validUnsent.toArray(validUnsentAddr); + + // copy valid addresses to validSent + validSentAddr = new Address[valid.size()]; + valid.toArray(validSentAddr); + } else { // all addresses pass + validSentAddr = addresses; + } + + + // print out the debug info + if (logger.isLoggable(Level.FINE)) { + if (validSentAddr != null && validSentAddr.length > 0) { + logger.fine("Verified Addresses"); + for (int l = 0; l < validSentAddr.length; l++) { + logger.fine(" " + validSentAddr[l]); + } + } + if (validUnsentAddr != null && validUnsentAddr.length > 0) { + logger.fine("Valid Unsent Addresses"); + for (int j = 0; j < validUnsentAddr.length; j++) { + logger.fine(" " + validUnsentAddr[j]); + } + } + if (invalidAddr != null && invalidAddr.length > 0) { + logger.fine("Invalid Addresses"); + for (int k = 0; k < invalidAddr.length; k++) { + logger.fine(" " + invalidAddr[k]); + } + } + } + + // throw the exception, fire TransportEvent.MESSAGE_NOT_DELIVERED event + if (sendFailed) { + logger.fine( + "Sending failed because of invalid destination addresses"); + notifyTransportListeners(TransportEvent.MESSAGE_NOT_DELIVERED, + validSentAddr, validUnsentAddr, + invalidAddr, this.message); + + // reset the connection so more sends are allowed + String lsr = lastServerResponse; // save, for get + int lrc = lastReturnCode; + try { + if (serverSocket != null) + issueCommand("RSET", -1); + } catch (MessagingException ex) { + // if can't reset, best to close the connection + try { + close(); + } catch (MessagingException ex2) { + // thrown by close()--ignore, will close() later anyway + logger.log(Level.FINE, "close failed", ex2); + } + } finally { + lastServerResponse = lsr; // restore + lastReturnCode = lrc; + } + + throw new SendFailedException("Invalid Addresses", mex, + validSentAddr, + validUnsentAddr, invalidAddr); + } + } + + /** + * Send the DATA command to the SMTP host and return + * an OutputStream to which the data is to be written. + * + * @return the stream to write to + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected OutputStream data() throws MessagingException { + assert Thread.holdsLock(this); + issueSendCommand("DATA", 354); + dataStream = new SMTPOutputStream(serverOutput); + return dataStream; + } + + /** + * Terminate the sent data. + * + * @exception IOException for I/O errors + * @exception MessagingException for other failures + * @since JavaMail 1.4.1 + */ + protected void finishData() throws IOException, MessagingException { + assert Thread.holdsLock(this); + dataStream.ensureAtBOL(); + issueSendCommand(".", 250); + } + + /** + * Return a stream that will use the SMTP BDAT command to send data. + * + * @return the stream to write to + * @exception MessagingException for failures + * @since JavaMail 1.6.0 + */ + protected OutputStream bdat() throws MessagingException { + assert Thread.holdsLock(this); + dataStream = new BDATOutputStream(serverOutput, chunkSize); + return dataStream; + } + + /** + * Terminate the sent data. + * + * @exception IOException for I/O errors + * @exception MessagingException for other failures + * @since JavaMail 1.6.0 + */ + protected void finishBdat() throws IOException, MessagingException { + assert Thread.holdsLock(this); + dataStream.ensureAtBOL(); + dataStream.close(); // doesn't close underlying socket + } + + /** + * Issue the STARTTLS command and switch the socket to + * TLS mode if it succeeds. + * + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void startTLS() throws MessagingException { + issueCommand("STARTTLS", 220); + // it worked, now switch the socket into TLS mode + try { + serverSocket = SocketFetcher.startTLS(serverSocket, host, + session.getProperties(), "mail." + name); + initStreams(); + } catch (IOException ioex) { + closeConnection(); + throw new MessagingException("Could not convert socket to TLS", + ioex); + } + } + + /////// primitives /////// + + /** + * Connect to host on port and start the SMTP protocol. + */ + private void openServer(String host, int port) + throws MessagingException { + + if (logger.isLoggable(Level.FINE)) + logger.fine("trying to connect to host \"" + host + + "\", port " + port + ", isSSL " + isSSL); + + try { + Properties props = session.getProperties(); + + serverSocket = SocketFetcher.getSocket(host, port, + props, "mail." + name, isSSL); + + // socket factory may've chosen a different port, + // update it for the debug messages that follow + port = serverSocket.getPort(); + // save host name for startTLS + this.host = host; + + initStreams(); + + int r = -1; + if ((r = readServerResponse()) != 220) { + String failResponse = lastServerResponse; + try { + if (quitOnSessionReject) { + sendCommand("QUIT"); + if (quitWait) { + int resp = readServerResponse(); + if (resp != 221 && resp != -1 && + logger.isLoggable(Level.FINE)) + logger.fine("QUIT failed with " + resp); + } + } + } catch (Exception e) { + if (logger.isLoggable(Level.FINE)) + logger.log(Level.FINE, "QUIT failed", e); + } finally { + serverSocket.close(); + serverSocket = null; + serverOutput = null; + serverInput = null; + lineInputStream = null; + } + if (logger.isLoggable(Level.FINE)) + logger.fine("got bad greeting from host \"" + + host + "\", port: " + port + + ", response: " + failResponse); + throw new MessagingException( + "Got bad greeting from SMTP host: " + host + + ", port: " + port + + ", response: " + failResponse); + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("connected to host \"" + + host + "\", port: " + port); + } + } catch (UnknownHostException uhex) { + throw new MessagingException("Unknown SMTP host: " + host, uhex); + } catch (SocketConnectException scex) { + throw new MailConnectException(scex); + } catch (IOException ioe) { + throw new MessagingException("Could not connect to SMTP host: " + + host + ", port: " + port, ioe); + } + } + + /** + * Start the protocol to the server on serverSocket, + * assumed to be provided and connected by the caller. + */ + private void openServer() throws MessagingException { + int port = -1; + host = "UNKNOWN"; + try { + port = serverSocket.getPort(); + host = serverSocket.getInetAddress().getHostName(); + if (logger.isLoggable(Level.FINE)) + logger.fine("starting protocol to host \"" + + host + "\", port " + port); + + initStreams(); + + int r = -1; + if ((r = readServerResponse()) != 220) { + try { + if (quitOnSessionReject) { + sendCommand("QUIT"); + if (quitWait) { + int resp = readServerResponse(); + if (resp != 221 && resp != -1 && + logger.isLoggable(Level.FINE)) + logger.fine("QUIT failed with " + resp); + } + } + } catch (Exception e) { + if (logger.isLoggable(Level.FINE)) + logger.log(Level.FINE, "QUIT failed", e); + } finally { + serverSocket.close(); + serverSocket = null; + serverOutput = null; + serverInput = null; + lineInputStream = null; + } + if (logger.isLoggable(Level.FINE)) + logger.fine("got bad greeting from host \"" + + host + "\", port: " + port + + ", response: " + r); + throw new MessagingException( + "Got bad greeting from SMTP host: " + host + + ", port: " + port + + ", response: " + r); + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("protocol started to host \"" + + host + "\", port: " + port); + } + } catch (IOException ioe) { + throw new MessagingException( + "Could not start protocol to SMTP host: " + + host + ", port: " + port, ioe); + } + } + + + private void initStreams() throws IOException { + boolean quote = PropUtil.getBooleanProperty(session.getProperties(), + "mail.debug.quote", false); + + traceInput = + new TraceInputStream(serverSocket.getInputStream(), traceLogger); + traceInput.setQuote(quote); + + traceOutput = + new TraceOutputStream(serverSocket.getOutputStream(), traceLogger); + traceOutput.setQuote(quote); + + serverOutput = + new BufferedOutputStream(traceOutput); + serverInput = + new BufferedInputStream(traceInput); + lineInputStream = new LineInputStream(serverInput); + } + + /** + * Is protocol tracing enabled? + */ + private boolean isTracing() { + return traceLogger.isLoggable(Level.FINEST); + } + + /** + * Temporarily turn off protocol tracing, e.g., to prevent + * tracing the authentication sequence, including the password. + */ + private void suspendTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(false); + traceOutput.setTrace(false); + } + } + + /** + * Resume protocol tracing, if it was enabled to begin with. + */ + private void resumeTracing() { + if (traceLogger.isLoggable(Level.FINEST)) { + traceInput.setTrace(true); + traceOutput.setTrace(true); + } + } + + /** + * Send the command to the server. If the expected response code + * is not received, throw a MessagingException. + * + * @param cmd the command to send + * @param expect the expected response code (-1 means don't care) + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + public synchronized void issueCommand(String cmd, int expect) + throws MessagingException { + sendCommand(cmd); + + // if server responded with an unexpected return code, + // throw the exception, notifying the client of the response + int resp = readServerResponse(); + if (expect != -1 && resp != expect) + throw new MessagingException(lastServerResponse); + } + + /** + * Issue a command that's part of sending a message. + */ + private void issueSendCommand(String cmd, int expect) + throws MessagingException { + sendCommand(cmd); + + // if server responded with an unexpected return code, + // throw the exception, notifying the client of the response + int ret; + if ((ret = readServerResponse()) != expect) { + // assume message was not sent to anyone, + // combine valid sent & unsent addresses + int vsl = validSentAddr == null ? 0 : validSentAddr.length; + int vul = validUnsentAddr == null ? 0 : validUnsentAddr.length; + Address[] valid = new Address[vsl + vul]; + if (vsl > 0) + System.arraycopy(validSentAddr, 0, valid, 0, vsl); + if (vul > 0) + System.arraycopy(validUnsentAddr, 0, valid, vsl, vul); + validSentAddr = null; + validUnsentAddr = valid; + if (logger.isLoggable(Level.FINE)) + logger.fine("got response code " + ret + + ", with response: " + lastServerResponse); + String _lsr = lastServerResponse; // else rset will nuke it + int _lrc = lastReturnCode; + if (serverSocket != null) // hasn't already been closed + issueCommand("RSET", -1); + lastServerResponse = _lsr; // restore, for get + lastReturnCode = _lrc; + throw new SMTPSendFailedException(cmd, ret, lastServerResponse, + exception, validSentAddr, validUnsentAddr, invalidAddr); + } + } + + /** + * Send the command to the server and return the response code + * from the server. + * + * @param cmd the command + * @return the response code + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + public synchronized int simpleCommand(String cmd) + throws MessagingException { + sendCommand(cmd); + return readServerResponse(); + } + + /** + * Send the command to the server and return the response code + * from the server. + * + * @param cmd the command + * @return the response code + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected int simpleCommand(byte[] cmd) throws MessagingException { + assert Thread.holdsLock(this); + sendCommand(cmd); + return readServerResponse(); + } + + /** + * Sends command cmd to the server terminating + * it with CRLF. + * + * @param cmd the command + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected void sendCommand(String cmd) throws MessagingException { + sendCommand(toBytes(cmd)); + } + + private void sendCommand(byte[] cmdBytes) throws MessagingException { + assert Thread.holdsLock(this); + //if (logger.isLoggable(Level.FINE)) + //logger.fine("SENT: " + new String(cmdBytes, 0)); + + try { + serverOutput.write(cmdBytes); + serverOutput.write(CRLF); + serverOutput.flush(); + } catch (IOException ex) { + throw new MessagingException("Can't send command to SMTP host", ex); + } + } + + /** + * Reads server reponse returning the returnCode + * as the number. Returns -1 on failure. Sets + * lastServerResponse and lastReturnCode. + * + * @return server response code + * @exception MessagingException for failures + * @since JavaMail 1.4.1 + */ + protected int readServerResponse() throws MessagingException { + assert Thread.holdsLock(this); + String serverResponse = ""; + int returnCode = 0; + StringBuilder buf = new StringBuilder(100); + + // read the server response line(s) and add them to the buffer + // that stores the response + try { + String line = null; + + do { + line = lineInputStream.readLine(); + if (line == null) { + serverResponse = buf.toString(); + if (serverResponse.length() == 0) + serverResponse = "[EOF]"; + lastServerResponse = serverResponse; + lastReturnCode = -1; + logger.log(Level.FINE, "EOF: {0}", serverResponse); + return -1; + } + buf.append(line); + buf.append("\n"); + } while (isNotLastLine(line)); + + serverResponse = buf.toString(); + } catch (IOException ioex) { + logger.log(Level.FINE, "exception reading response", ioex); + //ioex.printStackTrace(out); + lastServerResponse = ""; + lastReturnCode = 0; + throw new MessagingException("Exception reading response", ioex); + //returnCode = -1; + } + + // print debug info + //if (logger.isLoggable(Level.FINE)) + //logger.fine("RCVD: " + serverResponse); + + // parse out the return code + if (serverResponse.length() >= 3) { + try { + returnCode = Integer.parseInt(serverResponse.substring(0, 3)); + } catch (NumberFormatException nfe) { + try { + close(); + } catch (MessagingException mex) { + // thrown by close()--ignore, will close() later anyway + logger.log(Level.FINE, "close failed", mex); + } + returnCode = -1; + } catch (StringIndexOutOfBoundsException ex) { + try { + close(); + } catch (MessagingException mex) { + // thrown by close()--ignore, will close() later anyway + logger.log(Level.FINE, "close failed", mex); + } + returnCode = -1; + } + } else { + returnCode = -1; + } + if (returnCode == -1) + logger.log(Level.FINE, "bad server response: {0}", serverResponse); + + lastServerResponse = serverResponse; + lastReturnCode = returnCode; + return returnCode; + } + + /** + * Check if we're in the connected state. Don't bother checking + * whether the server is still alive, that will be detected later. + * + * @exception IllegalStateException if not connected + * + * @since JavaMail 1.4.1 + */ + protected void checkConnected() { + if (!super.isConnected()) + throw new IllegalStateException("Not connected"); + } + + // tests if the line is an intermediate line according to SMTP + private boolean isNotLastLine(String line) { + return line != null && line.length() >= 4 && line.charAt(3) == '-'; + } + + // wraps an address in "<>"'s if necessary + private String normalizeAddress(String addr) { + if ((!addr.startsWith("<")) && (!addr.endsWith(">"))) + return "<" + addr + ">"; + else + return addr; + } + + /** + * Return true if the SMTP server supports the specified service + * extension. Extensions are reported as results of the EHLO + * command when connecting to the server. See + * RFC 1869 + * and other RFCs that define specific extensions. + * + * @param ext the service extension name + * @return true if the extension is supported + * + * @since JavaMail 1.3.2 + */ + public boolean supportsExtension(String ext) { + return extMap != null && + extMap.get(ext.toUpperCase(Locale.ENGLISH)) != null; + } + + /** + * Return the parameter the server provided for the specified + * service extension, or null if the extension isn't supported. + * + * @param ext the service extension name + * @return the extension parameter + * + * @since JavaMail 1.3.2 + */ + public String getExtensionParameter(String ext) { + return extMap == null ? null : + extMap.get(ext.toUpperCase(Locale.ENGLISH)); + } + + /** + * Does the server we're connected to support the specified + * authentication mechanism? Uses the extension information + * returned by the server from the EHLO command. + * + * @param auth the authentication mechanism + * @return true if the authentication mechanism is supported + * + * @since JavaMail 1.4.1 + */ + protected boolean supportsAuthentication(String auth) { + assert Thread.holdsLock(this); + if (extMap == null) + return false; + String a = extMap.get("AUTH"); + if (a == null) + return false; + StringTokenizer st = new StringTokenizer(a); + while (st.hasMoreTokens()) { + String tok = st.nextToken(); + if (tok.equalsIgnoreCase(auth)) + return true; + } + // hack for buggy servers that advertise capability incorrectly + if (auth.equalsIgnoreCase("LOGIN") && supportsExtension("AUTH=LOGIN")) { + logger.fine("use AUTH=LOGIN hack"); + return true; + } + return false; + } + + private static char[] hexchar = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + /** + * Convert a string to RFC 1891 xtext format. + * + *
+     *     xtext = *( xchar / hexchar )
+     *
+     *     xchar = any ASCII CHAR between "!" (33) and "~" (126) inclusive,
+     *          except for "+" and "=".
+     *
+     * ; "hexchar"s are intended to encode octets that cannot appear
+     * ; as ASCII characters within an esmtp-value.
+     *
+     *     hexchar = ASCII "+" immediately followed by two upper case
+     *          hexadecimal digits
+     * 
+ * + * @param s the string to convert + * @return the xtext format string + * @since JavaMail 1.4.1 + */ + // XXX - keeping this around only for compatibility + protected static String xtext(String s) { + return xtext(s, false); + } + + /** + * Like xtext(s), but allow UTF-8 strings. + * + * @param s the string to convert + * @param utf8 convert string to UTF-8 first? + * @return the xtext format string + * @since JavaMail 1.6.0 + */ + protected static String xtext(String s, boolean utf8) { + StringBuilder sb = null; + byte[] bytes; + if (utf8) + bytes = s.getBytes(StandardCharsets.UTF_8); + else + bytes = ASCIIUtility.getBytes(s); + for (int i = 0; i < bytes.length; i++) { + char c = (char)(((int)bytes[i])&0xff); + if (!utf8 && c >= 128) // not ASCII + throw new IllegalArgumentException( + "Non-ASCII character in SMTP submitter: " + s); + if (c < '!' || c > '~' || c == '+' || c == '=') { + // not printable ASCII + if (sb == null) { + sb = new StringBuilder(s.length() + 4); + sb.append(s.substring(0, i)); + } + sb.append('+'); + sb.append(hexchar[(((int)c)& 0xf0) >> 4]); + sb.append(hexchar[((int)c)& 0x0f]); + } else { + if (sb != null) + sb.append(c); + } + } + return sb != null ? sb.toString() : s; + } + + private String traceUser(String user) { + return debugusername ? user : ""; + } + + private String tracePassword(String password) { + return debugpassword ? password : + (password == null ? "" : ""); + } + + /** + * Convert the String to either ASCII or UTF-8 bytes + * depending on allowutf8. + */ + private byte[] toBytes(String s) { + if (allowutf8) + return s.getBytes(StandardCharsets.UTF_8); + else + // don't use StandardCharsets.US_ASCII because it rejects non-ASCII + return ASCIIUtility.getBytes(s); + } + + /* + * Probe points for GlassFish monitoring. + */ + private void sendMessageStart(String subject) { } + private void sendMessageEnd() { } + + + /** + * An SMTPOutputStream that wraps a ChunkedOutputStream. + */ + private class BDATOutputStream extends SMTPOutputStream { + + /** + * Create a BDATOutputStream that wraps a ChunkedOutputStream + * of the given size and built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream + * @param size the chunk size + */ + public BDATOutputStream(OutputStream out, int size) { + super(new ChunkedOutputStream(out, size)); + } + + /** + * Close this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void close() throws IOException { + out.close(); + } + } + + /** + * An OutputStream that buffers data in chunks and uses the + * RFC 3030 BDAT SMTP command to send each chunk. + */ + private class ChunkedOutputStream extends OutputStream { + private final OutputStream out; + private final byte[] buf; + private int count = 0; + + /** + * Create a ChunkedOutputStream built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream + * @param size the chunk size + */ + public ChunkedOutputStream(OutputStream out, int size) { + this.out = out; + buf = new byte[size]; + } + + /** + * Writes the specified byte to this output stream. + * + * @param b the byte to write + * @exception IOException for I/O errors + */ + @Override + public void write(int b) throws IOException { + buf[count++] = (byte)b; + if (count >= buf.length) + flush(); + } + + /** + * Writes len bytes to this output stream starting at off. + * + * @param b bytes to write + * @param off offset in array + * @param len number of bytes to write + * @exception IOException for I/O errors + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + while (len > 0) { + int size = Math.min(buf.length - count, len); + if (size == buf.length) { + // avoid the copy + bdat(b, off, size, false); + } else { + System.arraycopy(b, off, buf, count, size); + count += size; + } + off += size; + len -= size; + if (count >= buf.length) + flush(); + } + } + + /** + * Flush this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void flush() throws IOException { + bdat(buf, 0, count, false); + count = 0; + } + + /** + * Close this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void close() throws IOException { + bdat(buf, 0, count, true); + count = 0; + } + + /** + * Send the specified bytes using the BDAT command. + */ + private void bdat(byte[] b, int off, int len, boolean last) + throws IOException { + if (len > 0 || last) { + try { + if (last) + sendCommand("BDAT " + len + " LAST"); + else + sendCommand("BDAT " + len); + out.write(b, off, len); + out.flush(); + int ret = readServerResponse(); + if (ret != 250) + throw new IOException(lastServerResponse); + } catch (MessagingException mex) { + throw new IOException("BDAT write exception", mex); + } + } + } + } +} diff --git a/app/src/main/java/com/sun/mail/smtp/SaslAuthenticator.java b/app/src/main/java/com/sun/mail/smtp/SaslAuthenticator.java new file mode 100644 index 0000000000..2c2a645ebd --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/SaslAuthenticator.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.smtp; + +import javax.mail.MessagingException; + +/** + * Interface to make it easier to call SMTPSaslAuthenticator. + */ + +public interface SaslAuthenticator { + public boolean authenticate(String[] mechs, String realm, String authzid, + String u, String p) throws MessagingException; + +} diff --git a/app/src/main/java/com/sun/mail/smtp/package.html b/app/src/main/java/com/sun/mail/smtp/package.html new file mode 100644 index 0000000000..197755c64c --- /dev/null +++ b/app/src/main/java/com/sun/mail/smtp/package.html @@ -0,0 +1,835 @@ + + + + + + +com.sun.mail.smtp package + + + +An SMTP protocol provider for the Jakarta Mail API +that provides access to an SMTP server. +Refer to RFC 821 +for more information. +

+When sending a message, detailed information on each address that +fails is available in an +{@link com.sun.mail.smtp.SMTPAddressFailedException SMTPAddressFailedException} +chained off the top level +{@link javax.mail.SendFailedException SendFailedException} +that is thrown. +In addition, if the mail.smtp.reportsuccess property +is set, an +{@link com.sun.mail.smtp.SMTPAddressSucceededException +SMTPAddressSucceededException} +will be included in the list for each address that is successful. +Note that this will cause a top level +{@link javax.mail.SendFailedException SendFailedException} +to be thrown even though the send was successful. +

+

+The SMTP provider also supports ESMTP +(RFC 1651). +It can optionally use SMTP Authentication +(RFC 2554) +using the LOGIN, PLAIN, DIGEST-MD5, and NTLM mechanisms +(RFC 4616 +and RFC 2831). +

+

+To use SMTP authentication you'll need to set the mail.smtp.auth +property (see below) or provide the SMTP Transport +with a username and password when connecting to the SMTP server. You +can do this using one of the following approaches: +

+
    +
  • +

    +Provide an Authenticator object when creating your mail Session +and provide the username and password information during the +Authenticator callback. +

    +

    +Note that the mail.smtp.user property can be set to provide a +default username for the callback, but the password will still need to be +supplied explicitly. +

    +

    +This approach allows you to use the static Transport send method +to send messages. +

    +
  • +
  • +

    +Call the Transport connect method explicitly with username and +password arguments. +

    +

    +This approach requires you to explicitly manage a Transport object +and use the Transport sendMessage method to send the message. +The transport.java demo program demonstrates how to manage a Transport +object. The following is roughly equivalent to the static +Transport send method, but supplies the needed username and +password: +

    +
    +Transport tr = session.getTransport("smtp");
    +tr.connect(smtphost, username, password);
    +msg.saveChanges();	// don't forget this
    +tr.sendMessage(msg, msg.getAllRecipients());
    +tr.close();
    +
    +
  • +
+

+When using DIGEST-MD5 authentication, +you'll also need to supply an appropriate realm; +your mail server administrator can supply this information. +You can set this using the mail.smtp.sasl.realm property, +or the setSASLRealm method on SMTPTransport. +

+

+The SMTP protocol provider can use SASL +(RFC 2222) +authentication mechanisms on systems that support the +javax.security.sasl APIs, such as J2SE 5.0. +In addition to the SASL mechanisms that are built into +the SASL implementation, users can also provide additional +SASL mechanisms of their own design to support custom authentication +schemes. See the + +Java SASL API Programming and Deployment Guide for details. +Note that the current implementation doesn't support SASL mechanisms +that provide their own integrity or confidentiality layer. +

+

+Support for OAuth 2.0 authentication via the + +XOAUTH2 authentication mechanism is provided either through the SASL +support described above or as a built-in authentication mechanism in the +SMTP provider. +The OAuth 2.0 Access Token should be passed as the password for this mechanism. +See +OAuth2 Support for details. +

+

+SMTP can also optionally request Delivery Status Notifications +(RFC 1891). +The delivery status will typically be reported using +a "multipart/report" +(RFC 1892) +message type with a "message/delivery-status" +(RFC 1894) +part. +You can use the classes in the com.sun.mail.dsn package to +handle these MIME types. +Note that you'll need to include dsn.jar in your CLASSPATH +as this support is not included in mail.jar. +

+

+See below for the properties to enable these features. +

+

+Note also that THERE IS NOT SUFFICIENT DOCUMENTATION HERE TO USE THESE +FEATURES!!! You will need to read the appropriate RFCs mentioned above +to understand what these features do and how to use them. Don't just +start setting properties and then complain to us when it doesn't work +like you expect it to work. READ THE RFCs FIRST!!! +

+

+The SMTP protocol provider supports the CHUNKING extension defined in +RFC 3030. +Set the mail.smtp.chunksize property to the desired chunk +size in bytes. +If the server supports the CHUNKING extension, the BDAT command will be +used to send the message in chunksize pieces. Note that no pipelining is +done so this will be slower than sending the message in one piece. +Note also that the BINARYMIME extension described in RFC 3030 is NOT supported. +

+Properties +

+The SMTP protocol provider supports the following properties, +which may be set in the Jakarta Mail Session object. +The properties are always set as strings; the Type column describes +how the string is interpreted. For example, use +

+
+	props.put("mail.smtp.port", "888");
+
+

+to set the mail.smtp.port property, which is of type int. +

+

+Note that if you're using the "smtps" protocol to access SMTP over SSL, +all the properties would be named "mail.smtps.*". +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SMTP properties
NameTypeDescription
mail.smtp.userStringDefault user name for SMTP.
mail.smtp.hostStringThe SMTP server to connect to.
mail.smtp.portintThe SMTP server port to connect to, if the connect() method doesn't +explicitly specify one. Defaults to 25.
mail.smtp.connectiontimeoutintSocket connection timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.smtp.timeoutintSocket read timeout value in milliseconds. +This timeout is implemented by java.net.Socket. +Default is infinite timeout.
mail.smtp.writetimeoutintSocket write timeout value in milliseconds. +This timeout is implemented by using a +java.util.concurrent.ScheduledExecutorService per connection +that schedules a thread to close the socket if the timeout expires. +Thus, the overhead of using this timeout is one thread per connection. +Default is infinite timeout.
mail.smtp.fromString +Email address to use for SMTP MAIL command. This sets the envelope +return address. Defaults to msg.getFrom() or +InternetAddress.getLocalAddress(). NOTE: mail.smtp.user was previously +used for this. +
mail.smtp.localhostString +Local host name used in the SMTP HELO or EHLO command. +Defaults to InetAddress.getLocalHost().getHostName(). +Should not normally need to +be set if your JDK and your name service are configured properly. +
mail.smtp.localaddressString +Local address (host name) to bind to when creating the SMTP socket. +Defaults to the address picked by the Socket class. +Should not normally need to be set, but useful with multi-homed hosts +where it's important to pick a particular local address to bind to. +
mail.smtp.localportint +Local port number to bind to when creating the SMTP socket. +Defaults to the port number picked by the Socket class. +
mail.smtp.ehloboolean +If false, do not attempt to sign on with the EHLO command. Defaults to +true. Normally failure of the EHLO command will fallback to the HELO +command; this property exists only for servers that don't fail EHLO +properly or don't implement EHLO properly. +
mail.smtp.authbooleanIf true, attempt to authenticate the user using the AUTH command. +Defaults to false.
mail.smtp.auth.mechanismsString +If set, lists the authentication mechanisms to consider, and the order +in which to consider them. Only mechanisms supported by the server and +supported by the current implementation will be used. +The default is "LOGIN PLAIN DIGEST-MD5 NTLM", which includes all +the authentication mechanisms supported by the current implementation +except XOAUTH2. +
mail.smtp.auth.login.disablebooleanIf true, prevents use of the AUTH LOGIN command. +Default is false.
mail.smtp.auth.plain.disablebooleanIf true, prevents use of the AUTH PLAIN command. +Default is false.
mail.smtp.auth.digest-md5.disablebooleanIf true, prevents use of the AUTH DIGEST-MD5 command. +Default is false.
mail.smtp.auth.ntlm.disablebooleanIf true, prevents use of the AUTH NTLM command. +Default is false.
mail.smtp.auth.ntlm.domainString +The NTLM authentication domain. +
mail.smtp.auth.ntlm.flagsint +NTLM protocol-specific flags. +See +http://curl.haxx.se/rfc/ntlm.html#theNtlmFlags for details. +
mail.smtp.auth.xoauth2.disablebooleanIf true, prevents use of the AUTHENTICATE XOAUTH2 command. +Because the OAuth 2.0 protocol requires a special access token instead of +a password, this mechanism is disabled by default. Enable it by explicitly +setting this property to "false" or by setting the "mail.smtp.auth.mechanisms" +property to "XOAUTH2".
mail.smtp.submitterStringThe submitter to use in the AUTH tag in the MAIL FROM command. +Typically used by a mail relay to pass along information about the +original submitter of the message. +See also the {@link com.sun.mail.smtp.SMTPMessage#setSubmitter setSubmitter} +method of {@link com.sun.mail.smtp.SMTPMessage SMTPMessage}. +Mail clients typically do not use this. +
mail.smtp.dsn.notifyStringThe NOTIFY option to the RCPT command. Either NEVER, or some +combination of SUCCESS, FAILURE, and DELAY (separated by commas).
mail.smtp.dsn.retStringThe RET option to the MAIL command. Either FULL or HDRS.
mail.smtp.allow8bitmimeboolean +If set to true, and the server supports the 8BITMIME extension, text +parts of messages that use the "quoted-printable" or "base64" encodings +are converted to use "8bit" encoding if they follow the RFC2045 rules +for 8bit text. +
mail.smtp.sendpartialboolean +If set to true, and a message has some valid and some invalid +addresses, send the message anyway, reporting the partial failure with +a SendFailedException. If set to false (the default), the message is +not sent to any of the recipients if there is an invalid recipient +address. +
mail.smtp.sasl.enableboolean +If set to true, attempt to use the javax.security.sasl package to +choose an authentication mechanism for login. +Defaults to false. +
mail.smtp.sasl.mechanismsString +A space or comma separated list of SASL mechanism names to try +to use. +
mail.smtp.sasl.authorizationidString +The authorization ID to use in the SASL authentication. +If not set, the authentication ID (user name) is used. +
mail.smtp.sasl.realmStringThe realm to use with DIGEST-MD5 authentication.
mail.smtp.sasl.usecanonicalhostnameboolean +If set to true, the canonical host name returned by +{@link java.net.InetAddress#getCanonicalHostName InetAddress.getCanonicalHostName} +is passed to the SASL mechanism, instead of the host name used to connect. +Defaults to false. +
mail.smtp.quitwaitboolean +If set to false, the QUIT command is sent +and the connection is immediately closed. +If set to true (the default), causes the transport to wait +for the response to the QUIT command. +
mail.smtp.quitonsessionrejectboolean +If set to false (the default), on session initiation rejection the QUIT +command is not sent and the connection is immediately closed. +If set to true, causes the transport to send the QUIT command prior to +closing the connection. +
mail.smtp.reportsuccessboolean +If set to true, causes the transport to include an +{@link com.sun.mail.smtp.SMTPAddressSucceededException +SMTPAddressSucceededException} +for each address that is successful. +Note also that this will cause a +{@link javax.mail.SendFailedException SendFailedException} +to be thrown from the +{@link com.sun.mail.smtp.SMTPTransport#sendMessage sendMessage} +method of +{@link com.sun.mail.smtp.SMTPTransport SMTPTransport} +even if all addresses were correct and the message was sent +successfully. +
mail.smtp.socketFactorySocketFactory +If set to a class that implements the +javax.net.SocketFactory interface, this class +will be used to create SMTP sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.smtp.socketFactory.classString +If set, specifies the name of a class that implements the +javax.net.SocketFactory interface. This class +will be used to create SMTP sockets. +
mail.smtp.socketFactory.fallbackboolean +If set to true, failure to create a socket using the specified +socket factory class will cause the socket to be created using +the java.net.Socket class. +Defaults to true. +
mail.smtp.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.smtp.ssl.enableboolean +If set to true, use SSL to connect and use the SSL port by default. +Defaults to false for the "smtp" protocol and true for the "smtps" protocol. +
mail.smtp.ssl.checkserveridentityboolean +If set to true, check the server identity as specified by +RFC 2595. +These additional checks based on the content of the server's certificate +are intended to prevent man-in-the-middle attacks. +Defaults to false. +
mail.smtp.ssl.trustString +If set, and a socket factory hasn't been specified, enables use of a +{@link com.sun.mail.util.MailSSLSocketFactory MailSSLSocketFactory}. +If set to "*", all hosts are trusted. +If set to a whitespace separated list of hosts, those hosts are trusted. +Otherwise, trust depends on the certificate the server presents. +
mail.smtp.ssl.socketFactorySSLSocketFactory +If set to a class that extends the +javax.net.ssl.SSLSocketFactory class, this class +will be used to create SMTP SSL sockets. Note that this is an +instance of a class, not a name, and must be set using the +put method, not the setProperty method. +
mail.smtp.ssl.socketFactory.classString +If set, specifies the name of a class that extends the +javax.net.ssl.SSLSocketFactory class. This class +will be used to create SMTP SSL sockets. +
mail.smtp.ssl.socketFactory.portint +Specifies the port to connect to when using the specified socket +factory. +If not set, the default port will be used. +
mail.smtp.ssl.protocolsstring +Specifies the SSL protocols that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledProtocols method. +
mail.smtp.ssl.ciphersuitesstring +Specifies the SSL cipher suites that will be enabled for SSL connections. +The property value is a whitespace separated list of tokens acceptable +to the javax.net.ssl.SSLSocket.setEnabledCipherSuites method. +
mail.smtp.starttls.enableboolean +If true, enables the use of the STARTTLS command (if +supported by the server) to switch the connection to a TLS-protected +connection before issuing any login commands. +If the server does not support STARTTLS, the connection continues without +the use of TLS; see the +mail.smtp.starttls.required +property to fail if STARTTLS isn't supported. +Note that an appropriate trust store must configured so that the client +will trust the server's certificate. +Defaults to false. +
mail.smtp.starttls.requiredboolean +If true, requires the use of the STARTTLS command. +If the server doesn't support the STARTTLS command, or the command +fails, the connect method will fail. +Defaults to false. +
mail.smtp.proxy.hoststring +Specifies the host name of an HTTP web proxy server that will be used for +connections to the mail server. +
mail.smtp.proxy.portstring +Specifies the port number for the HTTP web proxy server. +Defaults to port 80. +
mail.smtp.proxy.userstring +Specifies the user name to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.smtp.proxy.passwordstring +Specifies the password to use to authenticate with the HTTP web proxy server. +By default, no authentication is done. +
mail.smtp.socks.hoststring +Specifies the host name of a SOCKS5 proxy server that will be used for +connections to the mail server. +
mail.smtp.socks.portstring +Specifies the port number for the SOCKS5 proxy server. +This should only need to be used if the proxy server is not using +the standard port number of 1080. +
mail.smtp.mailextensionString +Extension string to append to the MAIL command. +The extension string can be used to specify standard SMTP +service extensions as well as vendor-specific extensions. +Typically the application should use the +{@link com.sun.mail.smtp.SMTPTransport SMTPTransport} +method {@link com.sun.mail.smtp.SMTPTransport#supportsExtension +supportsExtension} +to verify that the server supports the desired service extension. +See RFC 1869 +and other RFCs that define specific extensions. +
mail.smtp.usersetboolean +If set to true, use the RSET command instead of the NOOP command +in the {@link javax.mail.Transport#isConnected isConnected} method. +In some cases sendmail will respond slowly after many NOOP commands; +use of RSET avoids this sendmail issue. +Defaults to false. +
mail.smtp.noop.strictboolean +If set to true (the default), insist on a 250 response code from the NOOP +command to indicate success. The NOOP command is used by the +{@link javax.mail.Transport#isConnected isConnected} method to determine +if the connection is still alive. +Some older servers return the wrong response code on success, some +servers don't implement the NOOP command at all and so always return +a failure code. Set this property to false to handle servers +that are broken in this way. +Normally, when a server times out a connection, it will send a 421 +response code, which the client will see as the response to the next +command it issues. +Some servers send the wrong failure response code when timing out a +connection. +Do not set this property to false when dealing with servers that are +broken in this way. +
+

+In general, applications should not need to use the classes in this +package directly. Instead, they should use the APIs defined by +javax.mail package (and subpackages). Applications should +never construct instances of SMTPTransport directly. +Instead, they should use the +Session method getTransport to acquire an +appropriate Transport object. +

+

+In addition to printing debugging output as controlled by the +{@link javax.mail.Session Session} configuration, +the com.sun.mail.smtp provider logs the same information using +{@link java.util.logging.Logger} as described in the following table: +

+ + + + + + + + + + + + + + + + + + + + + + + + + +
SMTP Loggers
Logger NameLogging LevelPurpose
com.sun.mail.smtpCONFIGConfiguration of the SMTPTransport
com.sun.mail.smtpFINEGeneral debugging output
com.sun.mail.smtp.protocolFINESTComplete protocol trace
+

+WARNING: The APIs unique to this package should be +considered EXPERIMENTAL. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +

+ + + diff --git a/app/src/main/java/com/sun/mail/util/ASCIIUtility.java b/app/src/main/java/com/sun/mail/util/ASCIIUtility.java new file mode 100644 index 0000000000..4ab7a5576d --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/ASCIIUtility.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.IOException; + +public class ASCIIUtility { + + // Private constructor so that this class is not instantiated + private ASCIIUtility() { } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed integer in the given radix . The range extends + * from start till, but not including end.

+ * + * Based on java.lang.Integer.parseInt() + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @param radix the radix + * @return the integer value + * @exception NumberFormatException for conversion errors + */ + public static int parseInt(byte[] b, int start, int end, int radix) + throws NumberFormatException { + if (b == null) + throw new NumberFormatException("null"); + + int result = 0; + boolean negative = false; + int i = start; + int limit; + int multmin; + int digit; + + if (end > start) { + if (b[i] == '-') { + negative = true; + limit = Integer.MIN_VALUE; + i++; + } else { + limit = -Integer.MAX_VALUE; + } + multmin = limit / radix; + if (i < end) { + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException( + "illegal number: " + toString(b, start, end) + ); + } else { + result = -digit; + } + } + while (i < end) { + // Accumulating negatively avoids surprises near MAX_VALUE + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException("illegal number"); + } + if (result < multmin) { + throw new NumberFormatException("illegal number"); + } + result *= radix; + if (result < limit + digit) { + throw new NumberFormatException("illegal number"); + } + result -= digit; + } + } else { + throw new NumberFormatException("illegal number"); + } + if (negative) { + if (i > start + 1) { + return result; + } else { /* Only got "-" */ + throw new NumberFormatException("illegal number"); + } + } else { + return -result; + } + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed integer . The range extends from + * start till, but not including end. + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @return the integer value + * @exception NumberFormatException for conversion errors + */ + public static int parseInt(byte[] b, int start, int end) + throws NumberFormatException { + return parseInt(b, start, end, 10); + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed long in the given radix . The range extends + * from start till, but not including end.

+ * + * Based on java.lang.Long.parseLong() + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @param radix the radix + * @return the long value + * @exception NumberFormatException for conversion errors + */ + public static long parseLong(byte[] b, int start, int end, int radix) + throws NumberFormatException { + if (b == null) + throw new NumberFormatException("null"); + + long result = 0; + boolean negative = false; + int i = start; + long limit; + long multmin; + int digit; + + if (end > start) { + if (b[i] == '-') { + negative = true; + limit = Long.MIN_VALUE; + i++; + } else { + limit = -Long.MAX_VALUE; + } + multmin = limit / radix; + if (i < end) { + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException( + "illegal number: " + toString(b, start, end) + ); + } else { + result = -digit; + } + } + while (i < end) { + // Accumulating negatively avoids surprises near MAX_VALUE + digit = Character.digit((char)b[i++], radix); + if (digit < 0) { + throw new NumberFormatException("illegal number"); + } + if (result < multmin) { + throw new NumberFormatException("illegal number"); + } + result *= radix; + if (result < limit + digit) { + throw new NumberFormatException("illegal number"); + } + result -= digit; + } + } else { + throw new NumberFormatException("illegal number"); + } + if (negative) { + if (i > start + 1) { + return result; + } else { /* Only got "-" */ + throw new NumberFormatException("illegal number"); + } + } else { + return -result; + } + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a signed long . The range extends from + * start till, but not including end.

+ * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @return the long value + * @exception NumberFormatException for conversion errors + */ + public static long parseLong(byte[] b, int start, int end) + throws NumberFormatException { + return parseLong(b, start, end, 10); + } + + /** + * Convert the bytes within the specified range of the given byte + * array into a String. The range extends from start + * till, but not including end. + * + * @param b the bytes + * @param start the first byte offset + * @param end the last byte offset + * @return the String + */ + public static String toString(byte[] b, int start, int end) { + int size = end - start; + char[] theChars = new char[size]; + + for (int i = 0, j = start; i < size; ) + theChars[i++] = (char)(b[j++]&0xff); + + return new String(theChars); + } + + /** + * Convert the bytes into a String. + * + * @param b the bytes + * @return the String + * @since JavaMail 1.4.4 + */ + public static String toString(byte[] b) { + return toString(b, 0, b.length); + } + + public static String toString(ByteArrayInputStream is) { + int size = is.available(); + char[] theChars = new char[size]; + byte[] bytes = new byte[size]; + + is.read(bytes, 0, size); + for (int i = 0; i < size;) + theChars[i] = (char)(bytes[i++]&0xff); + + return new String(theChars); + } + + + public static byte[] getBytes(String s) { + char [] chars= s.toCharArray(); + int size = chars.length; + byte[] bytes = new byte[size]; + + for (int i = 0; i < size;) + bytes[i] = (byte) chars[i++]; + return bytes; + } + + public static byte[] getBytes(InputStream is) throws IOException { + + int len; + int size = 1024; + byte [] buf; + + + if (is instanceof ByteArrayInputStream) { + size = is.available(); + buf = new byte[size]; + len = is.read(buf, 0, size); + } + else { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + buf = new byte[size]; + while ((len = is.read(buf, 0, size)) != -1) + bos.write(buf, 0, len); + buf = bos.toByteArray(); + } + return buf; + } +} diff --git a/app/src/main/java/com/sun/mail/util/BASE64DecoderStream.java b/app/src/main/java/com/sun/mail/util/BASE64DecoderStream.java new file mode 100644 index 0000000000..2559c748a7 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/BASE64DecoderStream.java @@ -0,0 +1,456 @@ +/* + * Copyright (c) 1997, 2020 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a BASE64 Decoder. It is implemented as + * a FilterInputStream, so one can just wrap this class around + * any input stream and read bytes from this filter. The decoding + * is done as the bytes are read out. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BASE64DecoderStream extends FilterInputStream { + // buffer of decoded bytes for single byte reads + private byte[] buffer = new byte[3]; + private int bufsize = 0; // size of the cache + private int index = 0; // index into the cache + + // buffer for almost 8K of typical 76 chars + CRLF lines, + // used by getByte method. this buffer contains encoded bytes. + private byte[] input_buffer = new byte[78*105]; + private int input_pos = 0; + private int input_len = 0;; + + private boolean ignoreErrors = false; + + /** + * Create a BASE64 decoder that decodes the specified input stream. + * The System property mail.mime.base64.ignoreerrors + * controls whether errors in the encoded data cause an exception + * or are ignored. The default is false (errors cause exception). + * + * @param in the input stream + */ + public BASE64DecoderStream(InputStream in) { + super(in); + // default to false + ignoreErrors = PropUtil.getBooleanSystemProperty( + "mail.mime.base64.ignoreerrors", false); + } + + /** + * Create a BASE64 decoder that decodes the specified input stream. + * + * @param in the input stream + * @param ignoreErrors ignore errors in encoded data? + */ + public BASE64DecoderStream(InputStream in, boolean ignoreErrors) { + super(in); + this.ignoreErrors = ignoreErrors; + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an int in the range 0 + * to 255. If no byte is available because the end of + * the stream has been reached, the value -1 is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read() throws IOException { + if (index >= bufsize) { + bufsize = decode(buffer, 0, buffer.length); + if (bufsize <= 0) // buffer is empty + return -1; + index = 0; // reset index into buffer + } + return buffer[index++] & 0xff; // Zero off the MSB + } + + /** + * Reads up to len decoded bytes of data from this input stream + * into an array of bytes. This method blocks until some input is + * available. + *

+ * + * @param buf the buffer into which the data is read. + * @param off the start offset of the data. + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read(byte[] buf, int off, int len) throws IOException { + if (len == 0) + return 0; + // empty out single byte read buffer + int off0 = off; + while (index < bufsize && len > 0) { + buf[off++] = buffer[index++]; + len--; + } + if (index >= bufsize) + bufsize = index = 0; + + int bsize = (len / 3) * 3; // round down to multiple of 3 bytes + if (bsize > 0) { + int size = decode(buf, off, bsize); + off += size; + len -= size; + + if (size != bsize) { // hit EOF? + if (off == off0) // haven't returned any data + return -1; + else // returned some data before hitting EOF + return off - off0; + } + } + + // finish up with a partial read if necessary + for (; len > 0; len--) { + int c = read(); + if (c == -1) // EOF + break; + buf[off++] = (byte)c; + } + + if (off == off0) // haven't returned any data + return -1; + else // returned some data before hitting EOF + return off - off0; + } + + /** + * Skips over and discards n bytes of data from this stream. + */ + @Override + public long skip(long n) throws IOException { + long skipped = 0; + while (n-- > 0 && read() >= 0) + skipped++; + return skipped; + } + + /** + * Tests if this input stream supports marks. Currently this class + * does not support marks + */ + @Override + public boolean markSupported() { + return false; // Maybe later .. + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. However, this figure is only + * a close approximation in case the original encoded stream + * contains embedded CRLFs; since the CRLFs are discarded, not decoded + */ + @Override + public int available() throws IOException { + // This is only an estimate, since in.available() + // might include CRLFs too .. + return ((in.available() * 3)/4 + (bufsize-index)); + } + + /** + * This character array provides the character to value map + * based on RFC1521. + */ + private final static char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+','/' // 7 + }; + + private final static byte pem_convert_array[] = new byte[256]; + + static { + for (int i = 0; i < 255; i++) + pem_convert_array[i] = -1; + for (int i = 0; i < pem_array.length; i++) + pem_convert_array[pem_array[i]] = (byte)i; + } + + /** + * The decoder algorithm. Most of the complexity here is dealing + * with error cases. Returns the number of bytes decoded, which + * may be zero. Decoding is done by filling an int with 4 6-bit + * values by shifting them in from the bottom and then extracting + * 3 8-bit bytes from the int by shifting them out from the bottom. + * + * @param outbuf the buffer into which to put the decoded bytes + * @param pos position in the buffer to start filling + * @param len the number of bytes to fill + * @return the number of bytes filled, always a multiple + * of three, and may be zero + * @exception IOException if the data is incorrectly formatted + */ + private int decode(byte[] outbuf, int pos, int len) throws IOException { + int pos0 = pos; + while (len >= 3) { + /* + * We need 4 valid base64 characters before we start decoding. + * We skip anything that's not a valid base64 character (usually + * just CRLF). + */ + int got = 0; + int val = 0; + while (got < 4) { + int i = getByte(); + if (i == -1 || i == -2) { + boolean atEOF; + if (i == -1) { + if (got == 0) + return pos - pos0; + if (!ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded stream: " + + "needed 4 valid base64 characters " + + "but only got " + got + " before EOF" + + recentChars()); + atEOF = true; // don't read any more + } else { // i == -2 + // found a padding character, we're at EOF + // XXX - should do something to make EOF "sticky" + if (got < 2 && !ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded stream: " + + "needed at least 2 valid base64 characters," + + " but only got " + got + + " before padding character (=)" + + recentChars()); + + // didn't get any characters before padding character? + if (got == 0) + return pos - pos0; + atEOF = false; // need to keep reading + } + + // pad partial result with zeroes + + // how many bytes will we produce on output? + // (got always < 4, so size always < 3) + int size = got - 1; + if (size == 0) + size = 1; + + // handle the one padding character we've seen + got++; + val <<= 6; + + while (got < 4) { + if (!atEOF) { + // consume the rest of the padding characters, + // filling with zeroes + i = getByte(); + if (i == -1) { + if (!ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded " + + "stream: hit EOF while looking for " + + "padding characters (=)" + + recentChars()); + } else if (i != -2) { + if (!ignoreErrors) + throw new DecodingException( + "BASE64Decoder: Error in encoded " + + "stream: found valid base64 " + + "character after a padding character " + + "(=)" + recentChars()); + } + } + val <<= 6; + got++; + } + + // now pull out however many valid bytes we got + val >>= 8; // always skip first one + if (size == 2) + outbuf[pos + 1] = (byte)(val & 0xff); + val >>= 8; + outbuf[pos] = (byte)(val & 0xff); + // len -= size; // not needed, return below + pos += size; + return pos - pos0; + } else { + // got a valid byte + val <<= 6; + got++; + val |= i; + } + } + + // read 4 valid characters, now extract 3 bytes + outbuf[pos + 2] = (byte)(val & 0xff); + val >>= 8; + outbuf[pos + 1] = (byte)(val & 0xff); + val >>= 8; + outbuf[pos] = (byte)(val & 0xff); + len -= 3; + pos += 3; + } + return pos - pos0; + } + + /** + * Read the next valid byte from the input stream. + * Buffer lots of data from underlying stream in input_buffer, + * for efficiency. + * + * @return the next byte, -1 on EOF, or -2 if next byte is '=' + * (padding at end of encoded data) + */ + private int getByte() throws IOException { + int c; + do { + if (input_pos >= input_len) { + try { + input_len = in.read(input_buffer); + } catch (EOFException ex) { + return -1; + } + if (input_len <= 0) + return -1; + input_pos = 0; + } + // get the next byte in the buffer + c = input_buffer[input_pos++] & 0xff; + // is it a padding byte? + if (c == '=') + return -2; + // no, convert it + c = pem_convert_array[c]; + // loop until we get a legitimate byte + } while (c == -1); + return c; + } + + /** + * Return the most recent characters, for use in an error message. + */ + private String recentChars() { + // reach into the input buffer and extract up to 10 + // recent characters, to help in debugging. + String errstr = ""; + int nc = input_pos > 10 ? 10 : input_pos; + if (nc > 0) { + errstr += ", the " + nc + + " most recent characters were: \""; + for (int k = input_pos - nc; k < input_pos; k++) { + char c = (char)(input_buffer[k] & 0xff); + switch (c) { + case '\r': errstr += "\\r"; break; + case '\n': errstr += "\\n"; break; + case '\t': errstr += "\\t"; break; + default: + if (c >= ' ' && c < 0177) + errstr += c; + else + errstr += ("\\" + (int)c); + } + } + errstr += "\""; + } + return errstr; + } + + /** + * Base64 decode a byte array. No line breaks are allowed. + * This method is suitable for short strings, such as those + * in the IMAP AUTHENTICATE protocol, but not to decode the + * entire content of a MIME part. + * + * NOTE: inbuf may only contain valid base64 characters. + * Whitespace is not ignored. + * + * @param inbuf the byte array + * @return the decoded byte array + */ + public static byte[] decode(byte[] inbuf) { + int size = (inbuf.length / 4) * 3; + if (size == 0) + return inbuf; + + if (inbuf[inbuf.length - 1] == '=') { + size--; + if (inbuf[inbuf.length - 2] == '=') + size--; + } + byte[] outbuf = new byte[size]; + + int inpos = 0, outpos = 0; + size = inbuf.length; + while (size > 0) { + int val; + int osize = 3; + val = pem_convert_array[inbuf[inpos++] & 0xff]; + val <<= 6; + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + val <<= 6; + if (inbuf[inpos] != '=') // End of this BASE64 encoding + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + else + osize--; + val <<= 6; + if (inbuf[inpos] != '=') // End of this BASE64 encoding + val |= pem_convert_array[inbuf[inpos++] & 0xff]; + else + osize--; + if (osize > 2) + outbuf[outpos + 2] = (byte)(val & 0xff); + val >>= 8; + if (osize > 1) + outbuf[outpos + 1] = (byte)(val & 0xff); + val >>= 8; + outbuf[outpos] = (byte)(val & 0xff); + outpos += osize; + size -= 4; + } + return outbuf; + } + + /*** begin TEST program *** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + BASE64DecoderStream decoder = new BASE64DecoderStream(infile); + int c; + + while ((c = decoder.read()) != -1) + System.out.print((char)c); + System.out.flush(); + } + *** end TEST program ***/ +} diff --git a/app/src/main/java/com/sun/mail/util/BASE64EncoderStream.java b/app/src/main/java/com/sun/mail/util/BASE64EncoderStream.java new file mode 100644 index 0000000000..036b534821 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/BASE64EncoderStream.java @@ -0,0 +1,306 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a BASE64 encoder. It is implemented as + * a FilterOutputStream, so one can just wrap this class around + * any output stream and write bytes into this filter. The encoding + * is done as the bytes are written out. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BASE64EncoderStream extends FilterOutputStream { + private byte[] buffer; // cache of bytes that are yet to be encoded + private int bufsize = 0; // size of the cache + private byte[] outbuf; // line size output buffer + private int count = 0; // number of bytes that have been output + private int bytesPerLine; // number of bytes per line + private int lineLimit; // number of input bytes to output bytesPerLine + private boolean noCRLF = false; + + private static byte[] newline = new byte[] { '\r', '\n' }; + + /** + * Create a BASE64 encoder that encodes the specified output stream. + * + * @param out the output stream + * @param bytesPerLine number of bytes per line. The encoder inserts + * a CRLF sequence after the specified number of bytes, + * unless bytesPerLine is Integer.MAX_VALUE, in which + * case no CRLF is inserted. bytesPerLine is rounded + * down to a multiple of 4. + */ + public BASE64EncoderStream(OutputStream out, int bytesPerLine) { + super(out); + buffer = new byte[3]; + if (bytesPerLine == Integer.MAX_VALUE || bytesPerLine < 4) { + noCRLF = true; + bytesPerLine = 76; + } + bytesPerLine = (bytesPerLine / 4) * 4; // Rounded down to 4n + this.bytesPerLine = bytesPerLine; // save it + lineLimit = bytesPerLine / 4 * 3; + + if (noCRLF) { + outbuf = new byte[bytesPerLine]; + } else { + outbuf = new byte[bytesPerLine + 2]; + outbuf[bytesPerLine] = (byte)'\r'; + outbuf[bytesPerLine + 1] = (byte)'\n'; + } + } + + /** + * Create a BASE64 encoder that encodes the specified input stream. + * Inserts the CRLF sequence after outputting 76 bytes. + * + * @param out the output stream + */ + public BASE64EncoderStream(OutputStream out) { + this(out, 76); + } + + /** + * Encodes len bytes from the specified + * byte array starting at offset off to + * this output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized void write(byte[] b, int off, int len) + throws IOException { + int end = off + len; + + // finish off incomplete coding unit + while (bufsize != 0 && off < end) + write(b[off++]); + + // finish off line + int blen = ((bytesPerLine - count) / 4) * 3; + if (off + blen <= end) { + // number of bytes that will be produced in outbuf + int outlen = encodedSize(blen); + if (!noCRLF) { + outbuf[outlen++] = (byte)'\r'; + outbuf[outlen++] = (byte)'\n'; + } + out.write(encode(b, off, blen, outbuf), 0, outlen); + off += blen; + count = 0; + } + + // do bulk encoding a line at a time. + for (; off + lineLimit <= end; off += lineLimit) + out.write(encode(b, off, lineLimit, outbuf)); + + // handle remaining partial line + if (off + 3 <= end) { + blen = end - off; + blen = (blen / 3) * 3; // round down + // number of bytes that will be produced in outbuf + int outlen = encodedSize(blen); + out.write(encode(b, off, blen, outbuf), 0, outlen); + off += blen; + count += outlen; + } + + // start next coding unit + for (; off < end; off++) + write(b[off]); + } + + /** + * Encodes b.length bytes to this output stream. + * + * @param b the data to be written. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** + * Encodes the specified byte to this output stream. + * + * @param c the byte. + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized void write(int c) throws IOException { + buffer[bufsize++] = (byte)c; + if (bufsize == 3) { // Encoding unit = 3 bytes + encode(); + bufsize = 0; + } + } + + /** + * Flushes this output stream and forces any buffered output bytes + * to be encoded out to the stream. + * + * @exception IOException if an I/O error occurs. + */ + @Override + public synchronized void flush() throws IOException { + if (bufsize > 0) { // If there's unencoded characters in the buffer .. + encode(); // .. encode them + bufsize = 0; + } + out.flush(); + } + + /** + * Forces any buffered output bytes to be encoded out to the stream + * and closes this output stream + */ + @Override + public synchronized void close() throws IOException { + flush(); + if (count > 0 && !noCRLF) { + out.write(newline); + out.flush(); + } + out.close(); + } + + /** This array maps the characters to their 6 bit values */ + private final static char pem_array[] = { + 'A','B','C','D','E','F','G','H', // 0 + 'I','J','K','L','M','N','O','P', // 1 + 'Q','R','S','T','U','V','W','X', // 2 + 'Y','Z','a','b','c','d','e','f', // 3 + 'g','h','i','j','k','l','m','n', // 4 + 'o','p','q','r','s','t','u','v', // 5 + 'w','x','y','z','0','1','2','3', // 6 + '4','5','6','7','8','9','+','/' // 7 + }; + + /** + * Encode the data stored in buffer. + * Uses outbuf to store the encoded + * data before writing. + * + * @exception IOException if an I/O error occurs. + */ + private void encode() throws IOException { + int osize = encodedSize(bufsize); + out.write(encode(buffer, 0, bufsize, outbuf), 0, osize); + // increment count + count += osize; + // If writing out this encoded unit caused overflow, + // start a new line. + if (count >= bytesPerLine) { + if (!noCRLF) + out.write(newline); + count = 0; + } + } + + /** + * Base64 encode a byte array. No line breaks are inserted. + * This method is suitable for short strings, such as those + * in the IMAP AUTHENTICATE protocol, but not to encode the + * entire content of a MIME part. + * + * @param inbuf the byte array + * @return the encoded byte array + */ + public static byte[] encode(byte[] inbuf) { + if (inbuf.length == 0) + return inbuf; + return encode(inbuf, 0, inbuf.length, null); + } + + /** + * Internal use only version of encode. Allow specifying which + * part of the input buffer to encode. If outbuf is non-null, + * it's used as is. Otherwise, a new output buffer is allocated. + */ + private static byte[] encode(byte[] inbuf, int off, int size, + byte[] outbuf) { + if (outbuf == null) + outbuf = new byte[encodedSize(size)]; + int inpos, outpos; + int val; + for (inpos = off, outpos = 0; size >= 3; size -= 3, outpos += 4) { + val = inbuf[inpos++] & 0xff; + val <<= 8; + val |= inbuf[inpos++] & 0xff; + val <<= 8; + val |= inbuf[inpos++] & 0xff; + outbuf[outpos+3] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+2] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+1] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+0] = (byte)pem_array[val & 0x3f]; + } + // done with groups of three, finish up any odd bytes left + if (size == 1) { + val = inbuf[inpos++] & 0xff; + val <<= 4; + outbuf[outpos+3] = (byte)'='; // pad character; + outbuf[outpos+2] = (byte)'='; // pad character; + outbuf[outpos+1] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+0] = (byte)pem_array[val & 0x3f]; + } else if (size == 2) { + val = inbuf[inpos++] & 0xff; + val <<= 8; + val |= inbuf[inpos++] & 0xff; + val <<= 2; + outbuf[outpos+3] = (byte)'='; // pad character; + outbuf[outpos+2] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+1] = (byte)pem_array[val & 0x3f]; + val >>= 6; + outbuf[outpos+0] = (byte)pem_array[val & 0x3f]; + } + return outbuf; + } + + /** + * Return the corresponding encoded size for the given number + * of bytes, not including any CRLF. + */ + private static int encodedSize(int size) { + return ((size + 2) / 3) * 4; + } + + /*** begin TEST program + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + BASE64EncoderStream encoder = new BASE64EncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + *** end TEST program **/ +} diff --git a/app/src/main/java/com/sun/mail/util/BEncoderStream.java b/app/src/main/java/com/sun/mail/util/BEncoderStream.java new file mode 100644 index 0000000000..5a43dd688c --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/BEncoderStream.java @@ -0,0 +1,50 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a 'B' Encoder as defined by RFC2047 for + * encoding MIME headers. It subclasses the BASE64EncoderStream + * class. + * + * @author John Mani + */ + +public class BEncoderStream extends BASE64EncoderStream { + + /** + * Create a 'B' encoder that encodes the specified input stream. + * @param out the output stream + */ + public BEncoderStream(OutputStream out) { + super(out, Integer.MAX_VALUE); // MAX_VALUE is 2^31, should + // suffice (!) to indicate that + // CRLFs should not be inserted + } + + /** + * Returns the length of the encoded version of this byte array. + * + * @param b the byte array + * @return the length + */ + public static int encodedLength(byte[] b) { + return ((b.length + 2)/3) * 4; + } +} diff --git a/app/src/main/java/com/sun/mail/util/CRLFOutputStream.java b/app/src/main/java/com/sun/mail/util/CRLFOutputStream.java new file mode 100644 index 0000000000..7cca3d7cc8 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/CRLFOutputStream.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + + +/** + * Convert lines into the canonical format, that is, terminate lines with the + * CRLF sequence. + * + * @author John Mani + */ +public class CRLFOutputStream extends FilterOutputStream { + protected int lastb = -1; + protected boolean atBOL = true; // at beginning of line? + private static final byte[] newline = { (byte)'\r', (byte)'\n' }; + + public CRLFOutputStream(OutputStream os) { + super(os); + } + + @Override + public void write(int b) throws IOException { + if (b == '\r') { + writeln(); + } else if (b == '\n') { + if (lastb != '\r') + writeln(); + } else { + out.write(b); + atBOL = false; + } + lastb = b; + } + + @Override + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + int start = off; + + len += off; + for (int i = start; i < len ; i++) { + if (b[i] == '\r') { + out.write(b, start, i - start); + writeln(); + start = i + 1; + } else if (b[i] == '\n') { + if (lastb != '\r') { + out.write(b, start, i - start); + writeln(); + } + start = i + 1; + } + lastb = b[i]; + } + if ((len - start) > 0) { + out.write(b, start, len - start); + atBOL = false; + } + } + + /* + * Just write out a new line, something similar to out.println() + */ + public void writeln() throws IOException { + out.write(newline); + atBOL = true; + } +} diff --git a/app/src/main/java/com/sun/mail/util/DecodingException.java b/app/src/main/java/com/sun/mail/util/DecodingException.java new file mode 100644 index 0000000000..fddd84f8fc --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/DecodingException.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; + +/** + * A special IOException that indicates a failure to decode data due + * to an error in the formatting of the data. This allows applications + * to distinguish decoding errors from other I/O errors. + * + * @author Bill Shannon + */ + +public class DecodingException extends IOException { + + private static final long serialVersionUID = -6913647794421459390L; + + /** + * Constructor. + * + * @param s the exception message + */ + public DecodingException(String s) { + super(s); + } +} diff --git a/app/src/main/java/com/sun/mail/util/DefaultProvider.java b/app/src/main/java/com/sun/mail/util/DefaultProvider.java new file mode 100644 index 0000000000..c47e128782 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/DefaultProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.lang.annotation.*; + +/** + * Annotation to mark the default providers that are part of Jakarta Mail. + * DO NOT use this on any provider made available independently. + * + * @author Bill Shannon + * @since Jakarta Mail 1.6.4 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface DefaultProvider { +} diff --git a/app/src/main/java/com/sun/mail/util/FolderClosedIOException.java b/app/src/main/java/com/sun/mail/util/FolderClosedIOException.java new file mode 100644 index 0000000000..5a4ef10ae4 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/FolderClosedIOException.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; +import javax.mail.Folder; + +/** + * A variant of FolderClosedException that can be thrown from methods + * that only throw IOException. The getContent method will catch this + * exception and translate it back to FolderClosedException. + * + * @author Bill Shannon + */ + +public class FolderClosedIOException extends IOException { + transient private Folder folder; + + private static final long serialVersionUID = 4281122580365555735L; + + /** + * Constructor + * @param folder the Folder + */ + public FolderClosedIOException(Folder folder) { + this(folder, null); + } + + /** + * Constructor + * @param folder the Folder + * @param message the detailed error message + */ + public FolderClosedIOException(Folder folder, String message) { + super(message); + this.folder = folder; + } + + /** + * Returns the dead Folder object + * + * @return the dead Folder + */ + public Folder getFolder() { + return folder; + } +} diff --git a/app/src/main/java/com/sun/mail/util/LineInputStream.java b/app/src/main/java/com/sun/mail/util/LineInputStream.java new file mode 100644 index 0000000000..cb068f3ffb --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/LineInputStream.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.CharacterCodingException; + +/** + * LineInputStream supports reading CRLF terminated lines that + * contain only US-ASCII characters from an input stream. Provides + * functionality that is similar to the deprecated + * DataInputStream.readLine(). Expected use is to read + * lines as String objects from an IMAP/SMTP/etc. stream.

+ * + * This class also supports UTF-8 data by calling the appropriate + * constructor. Or, if the System property mail.mime.allowutf8 + * is set to true, an attempt will be made to interpret the data as UTF-8, + * falling back to treating it as an 8-bit charset if that fails.

+ * + * LineInputStream is implemented as a FilterInputStream, so one can just + * wrap it around any input stream and read bytes from this filter. + * + * @author John Mani + * @author Bill Shannon + */ + +public class LineInputStream extends FilterInputStream { + + private boolean allowutf8; + private byte[] lineBuffer = null; // reusable byte buffer + private CharsetDecoder decoder; + + private static boolean defaultutf8 = + PropUtil.getBooleanSystemProperty("mail.mime.allowutf8", false); + private static int MAX_INCR = 1024*1024; // 1MB + + public LineInputStream(InputStream in) { + this(in, false); + } + + /** + * @param in the InputStream + * @param allowutf8 allow UTF-8 characters? + * @since JavaMail 1.6 + */ + public LineInputStream(InputStream in, boolean allowutf8) { + super(in); + this.allowutf8 = allowutf8; + if (!allowutf8 && defaultutf8) { + decoder = StandardCharsets.UTF_8.newDecoder(); + decoder.onMalformedInput(CodingErrorAction.REPORT); + decoder.onUnmappableCharacter(CodingErrorAction.REPORT); + } + } + + /** + * Read a line containing only ASCII characters from the input + * stream. A line is terminated by a CR or NL or CR-NL sequence. + * A common error is a CR-CR-NL sequence, which will also terminate + * a line. + * The line terminator is not returned as part of the returned + * String. Returns null if no data is available.

+ * + * This class is similar to the deprecated + * DataInputStream.readLine() + * + * @return the line + * @exception IOException for I/O errors + */ + @SuppressWarnings("deprecation") // for old String constructor + public String readLine() throws IOException { + //InputStream in = this.in; + byte[] buf = lineBuffer; + + if (buf == null) + buf = lineBuffer = new byte[128]; + + int c1; + int room = buf.length; + int offset = 0; + + while ((c1 = in.read()) != -1) { + if (c1 == '\n') // Got NL, outa here. + break; + else if (c1 == '\r') { + // Got CR, is the next char NL ? + boolean twoCRs = false; + if (in.markSupported()) + in.mark(2); + int c2 = in.read(); + if (c2 == '\r') { // discard extraneous CR + twoCRs = true; + c2 = in.read(); + } + if (c2 != '\n') { + /* + * If the stream supports it (which we hope will always + * be the case), reset to after the first CR. Otherwise, + * we wrap a PushbackInputStream around the stream so we + * can unread the characters we don't need. The only + * problem with that is that the caller might stop + * reading from this LineInputStream, throw it away, + * and then start reading from the underlying stream. + * If that happens, the pushed back characters will be + * lost forever. + */ + if (in.markSupported()) + in.reset(); + else { + if (!(in instanceof PushbackInputStream)) + in /*= this.in*/ = new PushbackInputStream(in, 2); + if (c2 != -1) + ((PushbackInputStream)in).unread(c2); + if (twoCRs) + ((PushbackInputStream)in).unread('\r'); + } + } + break; // outa here. + } + + // Not CR, NL or CR-NL ... + // .. Insert the byte into our byte buffer + if (--room < 0) { // No room, need to grow. + if (buf.length < MAX_INCR) + buf = new byte[buf.length * 2]; + else + buf = new byte[buf.length + MAX_INCR]; + room = buf.length - offset - 1; + System.arraycopy(lineBuffer, 0, buf, 0, offset); + lineBuffer = buf; + } + buf[offset++] = (byte)c1; + } + + if ((c1 == -1) && (offset == 0)) + return null; + + if (allowutf8) + return new String(buf, 0, offset, StandardCharsets.UTF_8); + else { + if (defaultutf8) { + // try to decode it as UTF-8 + try { + return decoder.decode(ByteBuffer.wrap(buf, 0, offset)). + toString(); + } catch (CharacterCodingException cex) { + // looks like it's not valid UTF-8 data, + // fall through and treat it as an 8-bit charset + } + } + return new String(buf, 0, 0, offset); + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/LineOutputStream.java b/app/src/main/java/com/sun/mail/util/LineOutputStream.java new file mode 100644 index 0000000000..d86f5ec22a --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/LineOutputStream.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.nio.charset.StandardCharsets; + +/** + * This class is to support writing out Strings as a sequence of bytes + * terminated by a CRLF sequence. The String must contain only US-ASCII + * characters.

+ * + * The expected use is to write out RFC822 style headers to an output + * stream.

+ * + * @author John Mani + * @author Bill Shannon + */ + +public class LineOutputStream extends FilterOutputStream { + private boolean allowutf8; + + private static byte[] newline; + + static { + newline = new byte[2]; + newline[0] = (byte)'\r'; + newline[1] = (byte)'\n'; + } + + public LineOutputStream(OutputStream out) { + this(out, false); + } + + /** + * @param out the OutputStream + * @param allowutf8 allow UTF-8 characters? + * @since JavaMail 1.6 + */ + public LineOutputStream(OutputStream out, boolean allowutf8) { + super(out); + this.allowutf8 = allowutf8; + } + + public void writeln(String s) throws IOException { + byte[] bytes; + if (allowutf8) + bytes = s.getBytes(StandardCharsets.UTF_8); + else + bytes = ASCIIUtility.getBytes(s); + out.write(bytes); + out.write(newline); + } + + public void writeln() throws IOException { + out.write(newline); + } +} diff --git a/app/src/main/java/com/sun/mail/util/LogOutputStream.java b/app/src/main/java/com/sun/mail/util/LogOutputStream.java new file mode 100644 index 0000000000..c1c31531f9 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/LogOutputStream.java @@ -0,0 +1,128 @@ +/* + * Copyright (c) 2008, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; + +/** + * Capture output lines and send them to the mail logger. + */ +public class LogOutputStream extends OutputStream { + protected MailLogger logger; + protected Level level; + + private int lastb = -1; + private byte[] buf = new byte[80]; + private int pos = 0; + + /** + * Log to the specified logger. + * + * @param logger the MailLogger + */ + public LogOutputStream(MailLogger logger) { + this.logger = logger; + this.level = Level.FINEST; + } + + @Override + public void write(int b) throws IOException { + if (!logger.isLoggable(level)) + return; + + if (b == '\r') { + logBuf(); + } else if (b == '\n') { + if (lastb != '\r') + logBuf(); + } else { + expandCapacity(1); + buf[pos++] = (byte)b; + } + lastb = b; + } + + @Override + public void write(byte b[]) throws IOException { + write(b, 0, b.length); + } + + @Override + public void write(byte b[], int off, int len) throws IOException { + int start = off; + + if (!logger.isLoggable(level)) + return; + len += off; + for (int i = start; i < len ; i++) { + if (b[i] == '\r') { + expandCapacity(i - start); + System.arraycopy(b, start, buf, pos, i - start); + pos += i - start; + logBuf(); + start = i + 1; + } else if (b[i] == '\n') { + if (lastb != '\r') { + expandCapacity(i - start); + System.arraycopy(b, start, buf, pos, i - start); + pos += i - start; + logBuf(); + } + start = i + 1; + } + lastb = b[i]; + } + if ((len - start) > 0) { + expandCapacity(len - start); + System.arraycopy(b, start, buf, pos, len - start); + pos += len - start; + } + } + + /** + * Log the specified message. + * Can be overridden by subclass to do different logging. + * + * @param msg the message to log + */ + protected void log(String msg) { + logger.log(level, msg); + } + + /** + * Convert the buffer to a string and log it. + */ + private void logBuf() { + String msg = new String(buf, 0, pos); + pos = 0; + log(msg); + } + + /** + * Ensure that the buffer can hold at least len bytes + * beyond the current position. + */ + private void expandCapacity(int len) { + while (pos + len > buf.length) { + byte[] nb = new byte[buf.length * 2]; + System.arraycopy(buf, 0, nb, 0, pos); + buf = nb; + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/MailConnectException.java b/app/src/main/java/com/sun/mail/util/MailConnectException.java new file mode 100644 index 0000000000..768e90f4f7 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/MailConnectException.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import javax.mail.MessagingException; + +/** + * A MessagingException that indicates a socket connection attempt failed. + * Unlike java.net.ConnectException, it includes details of what we + * were trying to connect to. The underlying exception is available + * as the "cause" of this exception. + * + * @see java.net.ConnectException + * @author Bill Shannon + * @since JavaMail 1.5.0 + */ + +public class MailConnectException extends MessagingException { + private String host; + private int port; + private int cto; + + private static final long serialVersionUID = -3818807731125317729L; + + /** + * Constructs a MailConnectException. + * + * @param cex the SocketConnectException with the details + */ + public MailConnectException(SocketConnectException cex) { + super( + "Couldn't connect to host, port: " + + cex.getHost() + ", " + cex.getPort() + + "; timeout " + cex.getConnectionTimeout() + + (cex.getMessage() != null ? ("; " + cex.getMessage()) : "")); + // extract the details and save them here + this.host = cex.getHost(); + this.port = cex.getPort(); + this.cto = cex.getConnectionTimeout(); + setNextException(cex.getException()); + } + + /** + * The host we were trying to connect to. + * + * @return the host + */ + public String getHost() { + return host; + } + + /** + * The port we were trying to connect to. + * + * @return the port + */ + public int getPort() { + return port; + } + + /** + * The timeout used for the connection attempt. + * + * @return the connection timeout + */ + public int getConnectionTimeout() { + return cto; + } +} diff --git a/app/src/main/java/com/sun/mail/util/MailLogger.java b/app/src/main/java/com/sun/mail/util/MailLogger.java new file mode 100644 index 0000000000..686f5cab6f --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/MailLogger.java @@ -0,0 +1,419 @@ +/* + * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.PrintStream; +import java.text.MessageFormat; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.mail.Session; + +/** + * A simplified logger used by Jakarta Mail to handle logging to a + * PrintStream and logging through a java.util.logging.Logger. + * If debug is set, messages are written to the PrintStream and + * prefixed by the specified prefix (which is not included in + * Logger messages). + * Messages are logged by the Logger based on the configuration + * of the logging system. + */ + +/* + * It would be so much simpler to just subclass Logger and override + * the log(LogRecord) method, as the javadocs suggest, but that doesn't + * work because Logger makes the decision about whether to log the message + * or not before calling the log(LogRecord) method. Instead, we just + * provide the few log methods we need here. + */ + +public final class MailLogger { + /** + * For log messages. + */ + private final Logger logger; + /** + * For debug output. + */ + private final String prefix; + /** + * Produce debug output? + */ + private final boolean debug; + /** + * Stream for debug output. + */ + private final PrintStream out; + + /** + * Construct a new MailLogger using the specified Logger name, + * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream. + * + * @param name the Logger name + * @param prefix the prefix for debug output, or null for none + * @param debug if true, write to PrintStream + * @param out the PrintStream to write to + */ + public MailLogger(String name, String prefix, boolean debug, + PrintStream out) { + logger = Logger.getLogger(name); + this.prefix = prefix; + this.debug = debug; + this.out = out != null ? out : System.out; + } + + /** + * Construct a new MailLogger using the specified class' package + * name as the Logger name, + * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream. + * + * @param clazz the Logger name is the package name of this class + * @param prefix the prefix for debug output, or null for none + * @param debug if true, write to PrintStream + * @param out the PrintStream to write to + */ + public MailLogger(Class clazz, String prefix, boolean debug, + PrintStream out) { + String name = packageOf(clazz); + logger = Logger.getLogger(name); + this.prefix = prefix; + this.debug = debug; + this.out = out != null ? out : System.out; + } + + /** + * Construct a new MailLogger using the specified class' package + * name combined with the specified subname as the Logger name, + * debug prefix (e.g., "DEBUG"), debug flag, and PrintStream. + * + * @param clazz the Logger name is the package name of this class + * @param subname the Logger name relative to this Logger name + * @param prefix the prefix for debug output, or null for none + * @param debug if true, write to PrintStream + * @param out the PrintStream to write to + */ + public MailLogger(Class clazz, String subname, String prefix, boolean debug, + PrintStream out) { + String name = packageOf(clazz) + "." + subname; + logger = Logger.getLogger(name); + this.prefix = prefix; + this.debug = debug; + this.out = out != null ? out : System.out; + } + + /** + * Construct a new MailLogger using the specified Logger name and + * debug prefix (e.g., "DEBUG"). Get the debug flag and PrintStream + * from the Session. + * + * @param name the Logger name + * @param prefix the prefix for debug output, or null for none + * @param session where to get the debug flag and PrintStream + */ + @Deprecated + public MailLogger(String name, String prefix, Session session) { + this(name, prefix, session.getDebug(), session.getDebugOut()); + } + + /** + * Construct a new MailLogger using the specified class' package + * name as the Logger name and the specified + * debug prefix (e.g., "DEBUG"). Get the debug flag and PrintStream + * from the Session. + * + * @param clazz the Logger name is the package name of this class + * @param prefix the prefix for debug output, or null for none + * @param session where to get the debug flag and PrintStream + */ + @Deprecated + public MailLogger(Class clazz, String prefix, Session session) { + this(clazz, prefix, session.getDebug(), session.getDebugOut()); + } + + /** + * Create a MailLogger that uses a Logger with the specified name + * and prefix. The new MailLogger uses the same debug flag and + * PrintStream as this MailLogger. + * + * @param name the Logger name + * @param prefix the prefix for debug output, or null for none + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getLogger(String name, String prefix) { + return new MailLogger(name, prefix, debug, out); + } + + /** + * Create a MailLogger using the specified class' package + * name as the Logger name and the specified prefix. + * The new MailLogger uses the same debug flag and + * PrintStream as this MailLogger. + * + * @param clazz the Logger name is the package name of this class + * @param prefix the prefix for debug output, or null for none + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getLogger(Class clazz, String prefix) { + return new MailLogger(clazz, prefix, debug, out); + } + + /** + * Create a MailLogger that uses a Logger whose name is composed + * of this MailLogger's name plus the specified sub-name, separated + * by a dot. The new MailLogger uses the new prefix for debug output. + * This is used primarily by the protocol trace code that wants a + * different prefix (none). + * + * @param subname the Logger name relative to this Logger name + * @param prefix the prefix for debug output, or null for none + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getSubLogger(String subname, String prefix) { + return new MailLogger(logger.getName() + "." + subname, prefix, + debug, out); + } + + /** + * Create a MailLogger that uses a Logger whose name is composed + * of this MailLogger's name plus the specified sub-name, separated + * by a dot. The new MailLogger uses the new prefix for debug output. + * This is used primarily by the protocol trace code that wants a + * different prefix (none). + * + * @param subname the Logger name relative to this Logger name + * @param prefix the prefix for debug output, or null for none + * @param debug the debug flag for the sub-logger + * @return a MailLogger for the given name and prefix. + */ + public MailLogger getSubLogger(String subname, String prefix, + boolean debug) { + return new MailLogger(logger.getName() + "." + subname, prefix, + debug, out); + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + */ + public void log(Level level, String msg) { + ifDebugOut(msg); + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg); + } + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + * @param param1 the additional parameter. + */ + public void log(Level level, String msg, Object param1) { + if (debug) { + msg = MessageFormat.format(msg, new Object[] { param1 }); + debugOut(msg); + } + + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, param1); + } + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + * @param params the message parameters. + */ + public void log(Level level, String msg, Object... params) { + if (debug) { + msg = MessageFormat.format(msg, params); + debugOut(msg); + } + + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, params); + } + } + + /** + * Log the message at the specified level using a format string. + * @param level the log level. + * @param msg the message format string. + * @param params the message parameters. + * + * @since JavaMail 1.5.4 + */ + public void logf(Level level, String msg, Object... params) { + msg = String.format(msg, params); + ifDebugOut(msg); + logger.log(level, msg); + } + + /** + * Log the message at the specified level. + * @param level the log level. + * @param msg the message. + * @param thrown the throwable to log. + */ + public void log(Level level, String msg, Throwable thrown) { + if (debug) { + if (thrown != null) { + debugOut(msg + ", THROW: "); + thrown.printStackTrace(out); + } else { + debugOut(msg); + } + } + + if (logger.isLoggable(level)) { + final StackTraceElement frame = inferCaller(); + logger.logp(level, frame.getClassName(), frame.getMethodName(), msg, thrown); + } + } + + /** + * Log a message at the CONFIG level. + * @param msg the message. + */ + public void config(String msg) { + log(Level.CONFIG, msg); + } + + /** + * Log a message at the FINE level. + * @param msg the message. + */ + public void fine(String msg) { + log(Level.FINE, msg); + } + + /** + * Log a message at the FINER level. + * @param msg the message. + */ + public void finer(String msg) { + log(Level.FINER, msg); + } + + /** + * Log a message at the FINEST level. + * @param msg the message. + */ + public void finest(String msg) { + log(Level.FINEST, msg); + } + + /** + * If "debug" is set, or our embedded Logger is loggable at the + * given level, return true. + * @param level the log level. + * @return true if loggable. + */ + public boolean isLoggable(Level level) { + return debug || logger.isLoggable(level); + } + + /** + * Common code to conditionally log debug statements. + * @param msg the message to log. + */ + private void ifDebugOut(String msg) { + if (debug) + debugOut(msg); + } + + /** + * Common formatting for debug output. + * @param msg the message to log. + */ + private void debugOut(String msg) { + if (prefix != null) + out.println(prefix + ": " + msg); + else + out.println(msg); + } + + /** + * Return the package name of the class. + * Sometimes there will be no Package object for the class, + * e.g., if the class loader hasn't created one (see Class.getPackage()). + * @param clazz the class source. + * @return the package name or an empty string. + */ + private String packageOf(Class clazz) { + Package p = clazz.getPackage(); + if (p != null) + return p.getName(); // hopefully the common case + String cname = clazz.getName(); + int i = cname.lastIndexOf('.'); + if (i > 0) + return cname.substring(0, i); + // no package name, now what? + return ""; + } + + /** + * A disadvantage of not being able to use Logger directly in Jakarta Mail + * code is that the "source class" information that Logger guesses will + * always refer to this class instead of our caller. This method + * duplicates what Logger does to try to find *our* caller, so that + * Logger doesn't have to do it (and get the wrong answer), and because + * our caller is what's wanted. + * @return StackTraceElement that logged the message. Treat as read-only. + */ + private StackTraceElement inferCaller() { + // Get the stack trace. + StackTraceElement stack[] = (new Throwable()).getStackTrace(); + // First, search back to a method in the Logger class. + int ix = 0; + while (ix < stack.length) { + StackTraceElement frame = stack[ix]; + String cname = frame.getClassName(); + if (isLoggerImplFrame(cname)) { + break; + } + ix++; + } + // Now search for the first frame before the "Logger" class. + while (ix < stack.length) { + StackTraceElement frame = stack[ix]; + String cname = frame.getClassName(); + if (!isLoggerImplFrame(cname)) { + // We've found the relevant frame. + return frame; + } + ix++; + } + // We haven't found a suitable frame, so just punt. This is + // OK as we are only committed to making a "best effort" here. + return new StackTraceElement(MailLogger.class.getName(), "log", + MailLogger.class.getName(), -1); + } + + /** + * Frames to ignore as part of the MailLogger to JUL bridge. + * @param cname the class name. + * @return true if the class name is part of the MailLogger bridge. + */ + private boolean isLoggerImplFrame(String cname) { + return MailLogger.class.getName().equals(cname); + } +} diff --git a/app/src/main/java/com/sun/mail/util/MailSSLSocketFactory.java b/app/src/main/java/com/sun/mail/util/MailSSLSocketFactory.java new file mode 100644 index 0000000000..2f6363244f --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/MailSSLSocketFactory.java @@ -0,0 +1,356 @@ +/* + * Copyright (c) 1997, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.net.*; +import java.security.*; +import java.security.cert.*; +import java.util.*; + +import javax.net.ssl.*; + +/** + * An SSL socket factory that makes it easier to specify trust. + * This socket factory can be configured to trust all hosts or + * trust a specific set of hosts, in which case the server's + * certificate isn't verified. Alternatively, a custom TrustManager + * can be supplied.

+ * + * An instance of this factory can be set as the value of the + * mail.<protocol>.ssl.socketFactory property. + * + * @since JavaMail 1.4.2 + * @author Stephan Sann + * @author Bill Shannon + */ +public class MailSSLSocketFactory extends SSLSocketFactory { + + /** Should all hosts be trusted? */ + private boolean trustAllHosts; + + /** String-array of trusted hosts */ + private String[] trustedHosts = null; + + /** Holds a SSLContext to get SSLSocketFactories from */ + private SSLContext sslcontext; + + /** Holds the KeyManager array to use */ + private KeyManager[] keyManagers; + + /** Holds the TrustManager array to use */ + private TrustManager[] trustManagers; + + /** Holds the SecureRandom to use */ + private SecureRandom secureRandom; + + /** Holds a SSLSocketFactory to pass all API-method-calls to */ + private SSLSocketFactory adapteeFactory = null; + + /** + * Initializes a new MailSSLSocketFactory. + * + * @throws GeneralSecurityException for security errors + */ + public MailSSLSocketFactory() throws GeneralSecurityException { + this("TLS"); + } + + /** + * Initializes a new MailSSLSocketFactory with a given protocol. + * Normally the protocol will be specified as "TLS". + * + * @param protocol The protocol to use + * @throws NoSuchAlgorithmException if given protocol is not supported + * @throws GeneralSecurityException for security errors + */ + public MailSSLSocketFactory(String protocol) + throws GeneralSecurityException { + + // By default we do NOT trust all hosts. + trustAllHosts = false; + + // Get an instance of an SSLContext. + sslcontext = SSLContext.getInstance(protocol); + + // Default properties to init the SSLContext + keyManagers = null; + trustManagers = new TrustManager[] { new MailTrustManager() }; + secureRandom = null; + + // Assemble a default SSLSocketFactory to delegate all API-calls to. + newAdapteeFactory(); + } + + + /** + * Gets an SSLSocketFactory based on the given (or default) + * KeyManager array, TrustManager array and SecureRandom and + * sets it to the instance var adapteeFactory. + * + * @throws KeyManagementException for key manager errors + */ + private synchronized void newAdapteeFactory() + throws KeyManagementException { + sslcontext.init(keyManagers, trustManagers, secureRandom); + + // Get SocketFactory and save it in our instance var + adapteeFactory = sslcontext.getSocketFactory(); + } + + /** + * @return the keyManagers + */ + public synchronized KeyManager[] getKeyManagers() { + return keyManagers.clone(); + } + + /** + * @param keyManagers the keyManagers to set + * @throws GeneralSecurityException for security errors + */ + public synchronized void setKeyManagers(KeyManager... keyManagers) + throws GeneralSecurityException { + this.keyManagers = keyManagers.clone(); + newAdapteeFactory(); + } + + /** + * @return the secureRandom + */ + public synchronized SecureRandom getSecureRandom() { + return secureRandom; + } + + /** + * @param secureRandom the secureRandom to set + * @throws GeneralSecurityException for security errors + */ + public synchronized void setSecureRandom(SecureRandom secureRandom) + throws GeneralSecurityException { + this.secureRandom = secureRandom; + newAdapteeFactory(); + } + + /** + * @return the trustManagers + */ + public synchronized TrustManager[] getTrustManagers() { + return trustManagers; + } + + /** + * @param trustManagers the trustManagers to set + * @throws GeneralSecurityException for security errors + */ + public synchronized void setTrustManagers(TrustManager... trustManagers) + throws GeneralSecurityException { + this.trustManagers = trustManagers; + newAdapteeFactory(); + } + + /** + * @return true if all hosts should be trusted + */ + public synchronized boolean isTrustAllHosts() { + return trustAllHosts; + } + + /** + * @param trustAllHosts should all hosts be trusted? + */ + public synchronized void setTrustAllHosts(boolean trustAllHosts) { + this.trustAllHosts = trustAllHosts; + } + + /** + * @return the trusted hosts + */ + public synchronized String[] getTrustedHosts() { + if (trustedHosts == null) + return null; + else + return trustedHosts.clone(); + } + + /** + * @param trustedHosts the hosts to trust + */ + public synchronized void setTrustedHosts(String... trustedHosts) { + if (trustedHosts == null) + this.trustedHosts = null; + else + this.trustedHosts = trustedHosts.clone(); + } + + /** + * After a successful conection to the server, this method is + * called to ensure that the server should be trusted. + * + * @param server name of the server we connected to + * @param sslSocket SSLSocket connected to the server + * @return true if "trustAllHosts" is set to true OR the server + * is contained in the "trustedHosts" array; + */ + public synchronized boolean isServerTrusted(String server, + SSLSocket sslSocket) { + + //System.out.println("DEBUG: isServerTrusted host " + server); + + // If "trustAllHosts" is set to true, we return true + if (trustAllHosts) + return true; + + // If the socket host is contained in the "trustedHosts" array, + // we return true + if (trustedHosts != null) + return Arrays.asList(trustedHosts).contains(server); // ignore case? + + // If we get here, trust of the server was verified by the trust manager + return true; + } + + + // SocketFactory methods + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#createSocket(java.net.Socket, + * java.lang.String, int, boolean) + */ + @Override + public synchronized Socket createSocket(Socket socket, String s, int i, + boolean flag) throws IOException { + return adapteeFactory.createSocket(socket, s, i, flag); + } + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#getDefaultCipherSuites() + */ + @Override + public synchronized String[] getDefaultCipherSuites() { + return adapteeFactory.getDefaultCipherSuites(); + } + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#getSupportedCipherSuites() + */ + @Override + public synchronized String[] getSupportedCipherSuites() { + return adapteeFactory.getSupportedCipherSuites(); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket() + */ + @Override + public synchronized Socket createSocket() throws IOException { + return adapteeFactory.createSocket(); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int, + * java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i, + InetAddress inetaddress1, int j) throws IOException { + return adapteeFactory.createSocket(inetaddress, i, inetaddress1, j); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i) + throws IOException { + return adapteeFactory.createSocket(inetaddress, i); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int, + * java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(String s, int i, + InetAddress inetaddress, int j) + throws IOException, UnknownHostException { + return adapteeFactory.createSocket(s, i, inetaddress, j); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int) + */ + @Override + public synchronized Socket createSocket(String s, int i) + throws IOException, UnknownHostException { + return adapteeFactory.createSocket(s, i); + } + + + // inner classes + + /** + * A default Trustmanager. + * + * @author Stephan Sann + */ + private class MailTrustManager implements X509TrustManager { + + /** A TrustManager to pass method calls to */ + private X509TrustManager adapteeTrustManager = null; + + /** + * Initializes a new TrustManager instance. + */ + private MailTrustManager() throws GeneralSecurityException { + TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509"); + tmf.init((KeyStore)null); + adapteeTrustManager = (X509TrustManager)tmf.getTrustManagers()[0]; + } + + /* (non-Javadoc) + * @see javax.net.ssl.X509TrustManager#checkClientTrusted( + * java.security.cert.X509Certificate[], java.lang.String) + */ + @Override + public void checkClientTrusted(X509Certificate[] certs, String authType) + throws CertificateException { + if (!(isTrustAllHosts() || getTrustedHosts() != null)) + adapteeTrustManager.checkClientTrusted(certs, authType); + } + + /* (non-Javadoc) + * @see javax.net.ssl.X509TrustManager#checkServerTrusted( + * java.security.cert.X509Certificate[], java.lang.String) + */ + @Override + public void checkServerTrusted(X509Certificate[] certs, String authType) + throws CertificateException { + + if (!(isTrustAllHosts() || getTrustedHosts() != null)) + adapteeTrustManager.checkServerTrusted(certs, authType); + } + + /* (non-Javadoc) + * @see javax.net.ssl.X509TrustManager#getAcceptedIssuers() + */ + @Override + public X509Certificate[] getAcceptedIssuers() { + return adapteeTrustManager.getAcceptedIssuers(); + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/MessageRemovedIOException.java b/app/src/main/java/com/sun/mail/util/MessageRemovedIOException.java new file mode 100644 index 0000000000..96840f773f --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/MessageRemovedIOException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; + +/** + * A variant of MessageRemovedException that can be thrown from methods + * that only throw IOException. The getContent method will catch this + * exception and translate it back to MessageRemovedException. + * + * @see javax.mail.Message#isExpunged() + * @see javax.mail.Message#getMessageNumber() + * @author Bill Shannon + */ + +public class MessageRemovedIOException extends IOException { + + private static final long serialVersionUID = 4280468026581616424L; + + /** + * Constructs a MessageRemovedIOException with no detail message. + */ + public MessageRemovedIOException() { + super(); + } + + /** + * Constructs a MessageRemovedIOException with the specified detail message. + * @param s the detail message + */ + public MessageRemovedIOException(String s) { + super(s); + } +} diff --git a/app/src/main/java/com/sun/mail/util/MimeUtil.java b/app/src/main/java/com/sun/mail/util/MimeUtil.java new file mode 100644 index 0000000000..6944249857 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/MimeUtil.java @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2010, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.lang.reflect.*; +import java.security.*; + +import javax.mail.internet.MimePart; + +/** + * General MIME-related utility methods. + * + * @author Bill Shannon + * @since JavaMail 1.4.4 + */ +public class MimeUtil { + + private static final Method cleanContentType; + + static { + Method meth = null; + try { + String cth = System.getProperty("mail.mime.contenttypehandler"); + if (cth != null) { + ClassLoader cl = getContextClassLoader(); + Class clsHandler = null; + if (cl != null) { + try { + clsHandler = Class.forName(cth, false, cl); + } catch (ClassNotFoundException cex) { } + } + if (clsHandler == null) + clsHandler = Class.forName(cth); + meth = clsHandler.getMethod("cleanContentType", + new Class[] { MimePart.class, String.class }); + } + } catch (ClassNotFoundException ex) { + // ignore it + } catch (NoSuchMethodException ex) { + // ignore it + } catch (RuntimeException ex) { + // ignore it + } finally { + cleanContentType = meth; + } + } + + // No one should instantiate this class. + private MimeUtil() { + } + + /** + * If a Content-Type handler has been specified, + * call it to clean up the Content-Type value. + * + * @param mp the MimePart + * @param contentType the Content-Type value + * @return the cleaned Content-Type value + */ + public static String cleanContentType(MimePart mp, String contentType) { + if (cleanContentType != null) { + try { + return (String)cleanContentType.invoke(null, + new Object[] { mp, contentType }); + } catch (Exception ex) { + return contentType; + } + } else + return contentType; + } + + /** + * Convenience method to get our context class loader. + * Assert any privileges we might have and then call the + * Thread.getContextClassLoader method. + */ + private static ClassLoader getContextClassLoader() { + return + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public ClassLoader run() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ex) { } + return cl; + } + }); + } +} diff --git a/app/src/main/java/com/sun/mail/util/PropUtil.java b/app/src/main/java/com/sun/mail/util/PropUtil.java new file mode 100644 index 0000000000..430b378692 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/PropUtil.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.util.*; +import javax.mail.Session; + +/** + * Utilities to make it easier to get property values. + * Properties can be strings or type-specific value objects. + * + * @author Bill Shannon + */ +public class PropUtil { + + // No one should instantiate this class. + private PropUtil() { + } + + /** + * Get an integer valued property. + * + * @param props the properties + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + public static int getIntProperty(Properties props, String name, int def) { + return getInt(getProp(props, name), def); + } + + /** + * Get a boolean valued property. + * + * @param props the properties + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + public static boolean getBooleanProperty(Properties props, + String name, boolean def) { + return getBoolean(getProp(props, name), def); + } + + /** + * Get an integer valued property. + * + * @param session the Session + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + @Deprecated + public static int getIntSessionProperty(Session session, + String name, int def) { + return getInt(getProp(session.getProperties(), name), def); + } + + /** + * Get a boolean valued property. + * + * @param session the Session + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + @Deprecated + public static boolean getBooleanSessionProperty(Session session, + String name, boolean def) { + return getBoolean(getProp(session.getProperties(), name), def); + } + + /** + * Get a boolean valued System property. + * + * @param name the property name + * @param def default value if property not found + * @return the property value + */ + public static boolean getBooleanSystemProperty(String name, boolean def) { + try { + return getBoolean(getProp(System.getProperties(), name), def); + } catch (SecurityException sex) { + // fall through... + } + + /* + * If we can't get the entire System Properties object because + * of a SecurityException, just ask for the specific property. + */ + try { + String value = System.getProperty(name); + if (value == null) + return def; + if (def) + return !value.equalsIgnoreCase("false"); + else + return value.equalsIgnoreCase("true"); + } catch (SecurityException sex) { + return def; + } + } + + /** + * Get the value of the specified property. + * If the "get" method returns null, use the getProperty method, + * which might cascade to a default Properties object. + */ + private static Object getProp(Properties props, String name) { + Object val = props.get(name); + if (val != null) + return val; + else + return props.getProperty(name); + } + + /** + * Interpret the value object as an integer, + * returning def if unable. + */ + private static int getInt(Object value, int def) { + if (value == null) + return def; + if (value instanceof String) { + try { + String s = (String)value; + if (s.startsWith("0x")) + return Integer.parseInt(s.substring(2), 16); + else + return Integer.parseInt(s); + } catch (NumberFormatException nfex) { } + } + if (value instanceof Integer) + return ((Integer)value).intValue(); + return def; + } + + /** + * Interpret the value object as a boolean, + * returning def if unable. + */ + private static boolean getBoolean(Object value, boolean def) { + if (value == null) + return def; + if (value instanceof String) { + /* + * If the default is true, only "false" turns it off. + * If the default is false, only "true" turns it on. + */ + if (def) + return !((String)value).equalsIgnoreCase("false"); + else + return ((String)value).equalsIgnoreCase("true"); + } + if (value instanceof Boolean) + return ((Boolean)value).booleanValue(); + return def; + } +} diff --git a/app/src/main/java/com/sun/mail/util/QDecoderStream.java b/app/src/main/java/com/sun/mail/util/QDecoderStream.java new file mode 100644 index 0000000000..19ea0c6785 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/QDecoderStream.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a Q Decoder as defined in RFC 2047 + * for decoding MIME headers. It subclasses the QPDecoderStream class. + * + * @author John Mani + */ + +public class QDecoderStream extends QPDecoderStream { + + /** + * Create a Q-decoder that decodes the specified input stream. + * @param in the input stream + */ + public QDecoderStream(InputStream in) { + super(in); + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an int in the range 0 + * to 255. If no byte is available because the end of + * the stream has been reached, the value -1 is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read() throws IOException { + int c = in.read(); + + if (c == '_') // Return '_' as ' ' + return ' '; + else if (c == '=') { + // QP Encoded atom. Get the next two bytes .. + ba[0] = (byte)in.read(); + ba[1] = (byte)in.read(); + // .. and decode them + try { + return ASCIIUtility.parseInt(ba, 0, 2, 16); + } catch (NumberFormatException nex) { + throw new DecodingException( + "QDecoder: Error in QP stream " + nex.getMessage()); + } + } else + return c; + } +} diff --git a/app/src/main/java/com/sun/mail/util/QEncoderStream.java b/app/src/main/java/com/sun/mail/util/QEncoderStream.java new file mode 100644 index 0000000000..659897aec3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/QEncoderStream.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a Q Encoder as defined by RFC 2047 for + * encoding MIME headers. It subclasses the QPEncoderStream class. + * + * @author John Mani + */ + +public class QEncoderStream extends QPEncoderStream { + + private String specials; + private static String WORD_SPECIALS = "=_?\"#$%&'(),.:;<>@[\\]^`{|}~"; + private static String TEXT_SPECIALS = "=_?"; + + /** + * Create a Q encoder that encodes the specified input stream + * @param out the output stream + * @param encodingWord true if we are Q-encoding a word within a + * phrase. + */ + public QEncoderStream(OutputStream out, boolean encodingWord) { + super(out, Integer.MAX_VALUE); // MAX_VALUE is 2^31, should + // suffice (!) to indicate that + // CRLFs should not be inserted + // when encoding rfc822 headers + + // a RFC822 "word" token has more restrictions than a + // RFC822 "text" token. + specials = encodingWord ? WORD_SPECIALS : TEXT_SPECIALS; + } + + /** + * Encodes the specified byte to this output stream. + * @param c the byte. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(int c) throws IOException { + c = c & 0xff; // Turn off the MSB. + if (c == ' ') + output('_', false); + else if (c < 040 || c >= 0177 || specials.indexOf(c) >= 0) + // Encoding required. + output(c, true); + else // No encoding required + output(c, false); + } + + /** + * Returns the length of the encoded version of this byte array. + * + * @param b the byte array + * @param encodingWord true if encoding words, false if encoding text + * @return the length + */ + public static int encodedLength(byte[] b, boolean encodingWord) { + int len = 0; + String specials = encodingWord ? WORD_SPECIALS: TEXT_SPECIALS; + for (int i = 0; i < b.length; i++) { + int c = b[i] & 0xff; // Mask off MSB + if (c < 040 || c >= 0177 || specials.indexOf(c) >= 0) + // needs encoding + len += 3; // Q-encoding is 1 -> 3 conversion + else + len++; + } + return len; + } + + /**** begin TEST program *** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + QEncoderStream encoder = new QEncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + *** end TEST program ***/ +} diff --git a/app/src/main/java/com/sun/mail/util/QPDecoderStream.java b/app/src/main/java/com/sun/mail/util/QPDecoderStream.java new file mode 100644 index 0000000000..284c6145ca --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/QPDecoderStream.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a QP Decoder. It is implemented as + * a FilterInputStream, so one can just wrap this class around + * any input stream and read bytes from this filter. The decoding + * is done as the bytes are read out. + * + * @author John Mani + */ + +public class QPDecoderStream extends FilterInputStream { + protected byte[] ba = new byte[2]; + protected int spaces = 0; + + /** + * Create a Quoted Printable decoder that decodes the specified + * input stream. + * @param in the input stream + */ + public QPDecoderStream(InputStream in) { + super(new PushbackInputStream(in, 2)); // pushback of size=2 + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an int in the range 0 + * to 255. If no byte is available because the end of + * the stream has been reached, the value -1 is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read() throws IOException { + if (spaces > 0) { + // We have cached space characters, return one + spaces--; + return ' '; + } + + int c = in.read(); + + if (c == ' ') { + // Got space, keep reading till we get a non-space char + while ((c = in.read()) == ' ') + spaces++; + + if (c == '\r' || c == '\n' || c == -1) + // If the non-space char is CR/LF/EOF, the spaces we got + // so far is junk introduced during transport. Junk 'em. + spaces = 0; + else { + // The non-space char is NOT CR/LF, the spaces are valid. + ((PushbackInputStream)in).unread(c); + c = ' '; + } + return c; // return either or + } + else if (c == '=') { + // QP Encoded atom. Decode the next two bytes + int a = in.read(); + + if (a == '\n') { + /* Hmm ... not really confirming QP encoding, but lets + * allow this as a LF terminated encoded line .. and + * consider this a soft linebreak and recurse to fetch + * the next char. + */ + return read(); + } else if (a == '\r') { + // Expecting LF. This forms a soft linebreak to be ignored. + int b = in.read(); + if (b != '\n') + /* Not really confirming QP encoding, but + * lets allow this as well. + */ + ((PushbackInputStream)in).unread(b); + return read(); + } else if (a == -1) { + // Not valid QP encoding, but we be nice and tolerant here ! + return -1; + } else { + ba[0] = (byte)a; + ba[1] = (byte)in.read(); + try { + return ASCIIUtility.parseInt(ba, 0, 2, 16); + } catch (NumberFormatException nex) { + /* + System.err.println( + "Illegal characters in QP encoded stream: " + + ASCIIUtility.toString(ba, 0, 2) + ); + */ + + ((PushbackInputStream)in).unread(ba); + return c; + } + } + } + return c; + } + + /** + * Reads up to len decoded bytes of data from this input stream + * into an array of bytes. This method blocks until some input is + * available. + *

+ * + * @param buf the buffer into which the data is read. + * @param off the start offset of the data. + * @param len the maximum number of bytes read. + * @return the total number of bytes read into the buffer, or + * -1 if there is no more data because the end of + * the stream has been reached. + * @exception IOException if an I/O error occurs. + */ + @Override + public int read(byte[] buf, int off, int len) throws IOException { + int i, c; + for (i = 0; i < len; i++) { + if ((c = read()) == -1) { + if (i == 0) // At end of stream, so we should + i = -1; // return -1 , NOT 0. + break; + } + buf[off+i] = (byte)c; + } + return i; + } + + /** + * Skips over and discards n bytes of data from this stream. + */ + @Override + public long skip(long n) throws IOException { + long skipped = 0; + while (n-- > 0 && read() >= 0) + skipped++; + return skipped; + } + + /** + * Tests if this input stream supports marks. Currently this class + * does not support marks + */ + @Override + public boolean markSupported() { + return false; + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. The QP algorithm does not permit + * a priori knowledge of the number of bytes after decoding, so + * this method just invokes the available method + * of the original input stream. + */ + @Override + public int available() throws IOException { + // This is bogus ! We don't really know how much + // bytes are available *after* decoding + return in.available(); + } + + /**** begin TEST program + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + QPDecoderStream decoder = new QPDecoderStream(infile); + int c; + + while ((c = decoder.read()) != -1) + System.out.print((char)c); + System.out.println(); + } + *** end TEST program ****/ +} diff --git a/app/src/main/java/com/sun/mail/util/QPEncoderStream.java b/app/src/main/java/com/sun/mail/util/QPEncoderStream.java new file mode 100644 index 0000000000..d39102dae6 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/QPEncoderStream.java @@ -0,0 +1,196 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a Quoted Printable Encoder. It is implemented as + * a FilterOutputStream, so one can just wrap this class around + * any output stream and write bytes into this filter. The Encoding + * is done as the bytes are written out. + * + * @author John Mani + */ + +public class QPEncoderStream extends FilterOutputStream { + private int count = 0; // number of bytes that have been output + private int bytesPerLine; // number of bytes per line + private boolean gotSpace = false; + private boolean gotCR = false; + + /** + * Create a QP encoder that encodes the specified input stream + * @param out the output stream + * @param bytesPerLine the number of bytes per line. The encoder + * inserts a CRLF sequence after this many number + * of bytes. + */ + public QPEncoderStream(OutputStream out, int bytesPerLine) { + super(out); + // Subtract 1 to account for the '=' in the soft-return + // at the end of a line + this.bytesPerLine = bytesPerLine - 1; + } + + /** + * Create a QP encoder that encodes the specified input stream. + * Inserts the CRLF sequence after outputting 76 bytes. + * @param out the output stream + */ + public QPEncoderStream(OutputStream out) { + this(out, 76); + } + + /** + * Encodes len bytes from the specified + * byte array starting at offset off to + * this output stream. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (int i = 0; i < len; i++) + write(b[off + i]); + } + + /** + * Encodes b.length bytes to this output stream. + * @param b the data to be written. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** + * Encodes the specified byte to this output stream. + * @param c the byte. + * @exception IOException if an I/O error occurs. + */ + @Override + public void write(int c) throws IOException { + c = c & 0xff; // Turn off the MSB. + if (gotSpace) { // previous character was + if (c == '\r' || c == '\n') + // if CR/LF, we need to encode the char + output(' ', true); + else // no encoding required, just output the char + output(' ', false); + gotSpace = false; + } + + if (c == '\r') { + gotCR = true; + outputCRLF(); + } else { + if (c == '\n') { + if (gotCR) + // This is a CRLF sequence, we already output the + // corresponding CRLF when we got the CR, so ignore this + ; + else + outputCRLF(); + } else if (c == ' ') { + gotSpace = true; + } else if (c < 040 || c >= 0177 || c == '=') + // Encoding required. + output(c, true); + else // No encoding required + output(c, false); + // whatever it was, it wasn't a CR + gotCR = false; + } + } + + /** + * Flushes this output stream and forces any buffered output bytes + * to be encoded out to the stream. + * @exception IOException if an I/O error occurs. + */ + @Override + public void flush() throws IOException { + if (gotSpace) { + output(' ', true); + gotSpace = false; + } + out.flush(); + } + + /** + * Forces any buffered output bytes to be encoded out to the stream + * and closes this output stream. + * + * @exception IOException for I/O errors + */ + @Override + public void close() throws IOException { + flush(); + out.close(); + } + + private void outputCRLF() throws IOException { + out.write('\r'); + out.write('\n'); + count = 0; + } + + // The encoding table + private final static char hex[] = { + '0','1', '2', '3', '4', '5', '6', '7', + '8','9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + + protected void output(int c, boolean encode) throws IOException { + if (encode) { + if ((count += 3) > bytesPerLine) { + out.write('='); + out.write('\r'); + out.write('\n'); + count = 3; // set the next line's length + } + out.write('='); + out.write(hex[c >> 4]); + out.write(hex[c & 0xf]); + } else { + if (++count > bytesPerLine) { + out.write('='); + out.write('\r'); + out.write('\n'); + count = 1; // set the next line's length + } + out.write(c); + } + } + + /**** begin TEST program *** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + QPEncoderStream encoder = new QPEncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + *** end TEST program ***/ +} diff --git a/app/src/main/java/com/sun/mail/util/ReadableMime.java b/app/src/main/java/com/sun/mail/util/ReadableMime.java new file mode 100644 index 0000000000..ecba7e2a49 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/ReadableMime.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.InputStream; + +import javax.mail.MessagingException; + +/** + * A Message or message Part whose data can be read as a MIME format + * stream. Note that the MIME stream will include both the headers + * and the body of the message or part. This should be the same data + * that is produced by the writeTo method, but in a readable form. + * + * @author Bill Shannon + * @since JavaMail 1.4.5 + */ +public interface ReadableMime { + /** + * Return the MIME format stream corresponding to this message part. + * + * @return the MIME format stream + * @exception MessagingException for failures + */ + public InputStream getMimeStream() throws MessagingException; +} diff --git a/app/src/main/java/com/sun/mail/util/SharedByteArrayOutputStream.java b/app/src/main/java/com/sun/mail/util/SharedByteArrayOutputStream.java new file mode 100644 index 0000000000..9fd617ea9b --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/SharedByteArrayOutputStream.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2012, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.InputStream; +import java.io.ByteArrayOutputStream; + +import javax.mail.util.SharedByteArrayInputStream; + +/** + * A ByteArrayOutputStream that allows us to share the byte array + * rather than copy it. Eventually could replace this with something + * that doesn't require a single contiguous byte array. + * + * @author Bill Shannon + * @since JavaMail 1.4.5 + */ +public class SharedByteArrayOutputStream extends ByteArrayOutputStream { + public SharedByteArrayOutputStream(int size) { + super(size); + } + + public InputStream toStream() { + return new SharedByteArrayInputStream(buf, 0, count); + } +} diff --git a/app/src/main/java/com/sun/mail/util/SocketConnectException.java b/app/src/main/java/com/sun/mail/util/SocketConnectException.java new file mode 100644 index 0000000000..6b3c16e082 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/SocketConnectException.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.IOException; + +/** + * An IOException that indicates a socket connection attempt failed. + * Unlike java.net.ConnectException, it includes details of what we + * were trying to connect to. + * + * @see java.net.ConnectException + * @author Bill Shannon + * @since JavaMail 1.5.0 + */ + +public class SocketConnectException extends IOException { + /** + * The socket host name. + */ + private String host; + /** + * The socket port. + */ + private int port; + /** + * The connection timeout. + */ + private int cto; + /** + * The generated serial id. + */ + private static final long serialVersionUID = 3997871560538755463L; + + /** + * Constructs a SocketConnectException. + * + * @param msg error message detail + * @param cause the underlying exception that indicates the failure + * @param host the host we were trying to connect to + * @param port the port we were trying to connect to + * @param cto the timeout for the connection attempt + */ + public SocketConnectException(String msg, Exception cause, + String host, int port, int cto) { + super(msg); + initCause(cause); + this.host = host; + this.port = port; + this.cto = cto; + } + + /** + * The exception that caused the failure. + * + * @return the exception + */ + public Exception getException() { + // the "cause" is always an Exception; see constructor above + Throwable t = getCause(); + assert t == null || t instanceof Exception; + return (Exception) t; + } + + /** + * The host we were trying to connect to. + * + * @return the host + */ + public String getHost() { + return host; + } + + /** + * The port we were trying to connect to. + * + * @return the port + */ + public int getPort() { + return port; + } + + /** + * The timeout used for the connection attempt. + * + * @return the connection timeout + */ + public int getConnectionTimeout() { + return cto; + } +} diff --git a/app/src/main/java/com/sun/mail/util/SocketFetcher.java b/app/src/main/java/com/sun/mail/util/SocketFetcher.java new file mode 100644 index 0000000000..d6af220615 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/SocketFetcher.java @@ -0,0 +1,895 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.security.*; +import java.net.*; +import java.io.*; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.lang.reflect.*; +import java.util.*; +import java.util.regex.*; +import java.util.logging.Level; +import java.security.cert.*; +import javax.net.*; +import javax.net.ssl.*; + +/** + * This class is used to get Sockets. Depending on the arguments passed + * it will either return a plain java.net.Socket or dynamically load + * the SocketFactory class specified in the classname param and return + * a socket created by that SocketFactory. + * + * @author Max Spivak + * @author Bill Shannon + */ +public class SocketFetcher { + + private static MailLogger logger = new MailLogger( + SocketFetcher.class, + "socket", + "DEBUG SocketFetcher", + PropUtil.getBooleanSystemProperty("mail.socket.debug", false), + System.out); + + // No one should instantiate this class. + private SocketFetcher() { + } + + /** + * This method returns a Socket. Properties control the use of + * socket factories and other socket characteristics. The properties + * used are: + *

    + *
  • prefix.socketFactory + *
  • prefix.socketFactory.class + *
  • prefix.socketFactory.fallback + *
  • prefix.socketFactory.port + *
  • prefix.ssl.socketFactory + *
  • prefix.ssl.socketFactory.class + *
  • prefix.ssl.socketFactory.port + *
  • prefix.timeout + *
  • prefix.connectiontimeout + *
  • prefix.localaddress + *
  • prefix.localport + *
  • prefix.usesocketchannels + *

+ * If we're making an SSL connection, the ssl.socketFactory + * properties are used first, if set.

+ * + * If the socketFactory property is set, the value is an + * instance of a SocketFactory class, not a string. The + * instance is used directly. If the socketFactory property + * is not set, the socketFactory.class property is considered. + * (Note that the SocketFactory property must be set using the + * put method, not the setProperty + * method.)

+ * + * If the socketFactory.class property isn't set, the socket + * returned is an instance of java.net.Socket connected to the + * given host and port. If the socketFactory.class property is set, + * it is expected to contain a fully qualified classname of a + * javax.net.SocketFactory subclass. In this case, the class is + * dynamically instantiated and a socket created by that + * SocketFactory is returned.

+ * + * If the socketFactory.fallback property is set to false, don't + * fall back to using regular sockets if the socket factory fails.

+ * + * The socketFactory.port specifies a port to use when connecting + * through the socket factory. If unset, the port argument will be + * used.

+ * + * If the connectiontimeout property is set, the timeout is passed + * to the socket connect method.

+ * + * If the timeout property is set, it is used to set the socket timeout. + *

+ * + * If the localaddress property is set, it's used as the local address + * to bind to. If the localport property is also set, it's used as the + * local port number to bind to.

+ * + * If the usesocketchannels property is set, and we create the Socket + * ourself, and the selection of other properties allows, create a + * SocketChannel and get the Socket from it. This allows us to later + * retrieve the SocketChannel from the Socket and use it with Select. + * + * @param host The host to connect to + * @param port The port to connect to at the host + * @param props Properties object containing socket properties + * @param prefix Property name prefix, e.g., "mail.imap" + * @param useSSL use the SSL socket factory as the default + * @return the Socket + * @exception IOException for I/O errors + */ + public static Socket getSocket(String host, int port, Properties props, + String prefix, boolean useSSL) + throws IOException { + + if (logger.isLoggable(Level.FINER)) + logger.finer("getSocket" + ", host " + host + ", port " + port + + ", prefix " + prefix + ", useSSL " + useSSL); + if (prefix == null) + prefix = "socket"; + if (props == null) + props = new Properties(); // empty + int cto = PropUtil.getIntProperty(props, + prefix + ".connectiontimeout", -1); + Socket socket = null; + String localaddrstr = props.getProperty(prefix + ".localaddress", null); + InetAddress localaddr = null; + if (localaddrstr != null) + localaddr = InetAddress.getByName(localaddrstr); + int localport = PropUtil.getIntProperty(props, + prefix + ".localport", 0); + + boolean fb = PropUtil.getBooleanProperty(props, + prefix + ".socketFactory.fallback", true); + + int sfPort = -1; + String sfErr = "unknown socket factory"; + int to = PropUtil.getIntProperty(props, prefix + ".timeout", -1); + try { + /* + * If using SSL, first look for SSL-specific class name or + * factory instance. + */ + SocketFactory sf = null; + String sfPortName = null; + if (useSSL) { + Object sfo = props.get(prefix + ".ssl.socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "SSL socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".ssl.socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "SSL socket factory class " + sfClass; + } + sfPortName = ".ssl.socketFactory.port"; + } + + if (sf == null) { + Object sfo = props.get(prefix + ".socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "socket factory class " + sfClass; + } + sfPortName = ".socketFactory.port"; + } + + // if we now have a socket factory, use it + if (sf != null) { + sfPort = PropUtil.getIntProperty(props, + prefix + sfPortName, -1); + + // if port passed in via property isn't valid, use param + if (sfPort == -1) + sfPort = port; + socket = createSocket(localaddr, localport, + host, sfPort, cto, to, props, prefix, sf, useSSL); + } + } catch (SocketTimeoutException sex) { + throw sex; + } catch (Exception ex) { + if (!fb) { + if (ex instanceof InvocationTargetException) { + Throwable t = + ((InvocationTargetException)ex).getTargetException(); + if (t instanceof Exception) + ex = (Exception)t; + } + if (ex instanceof IOException) + throw (IOException)ex; + throw new SocketConnectException("Using " + sfErr, ex, + host, sfPort, cto); + } + } + + if (socket == null) { + socket = createSocket(localaddr, localport, + host, port, cto, to, props, prefix, null, useSSL); + + } else { + if (to >= 0) { + if (logger.isLoggable(Level.FINEST)) + logger.finest("set socket read timeout " + to); + socket.setSoTimeout(to); + } + } + + return socket; + } + + public static Socket getSocket(String host, int port, Properties props, + String prefix) throws IOException { + return getSocket(host, port, props, prefix, false); + } + + /** + * Create a socket with the given local address and connected to + * the given host and port. Use the specified connection timeout + * and read timeout. + * If a socket factory is specified, use it. Otherwise, use the + * SSLSocketFactory if useSSL is true. + */ + private static Socket createSocket(InetAddress localaddr, int localport, + String host, int port, int cto, int to, + Properties props, String prefix, + SocketFactory sf, boolean useSSL) + throws IOException { + Socket socket = null; + + if (logger.isLoggable(Level.FINEST)) + logger.finest("create socket: prefix " + prefix + + ", localaddr " + localaddr + ", localport " + localport + + ", host " + host + ", port " + port + + ", connection timeout " + cto + ", timeout " + to + + ", socket factory " + sf + ", useSSL " + useSSL); + + String proxyHost = props.getProperty(prefix + ".proxy.host", null); + String proxyUser = props.getProperty(prefix + ".proxy.user", null); + String proxyPassword = props.getProperty(prefix + ".proxy.password", null); + int proxyPort = 80; + String socksHost = null; + int socksPort = 1080; + String err = null; + + if (proxyHost != null) { + int i = proxyHost.indexOf(':'); + if (i >= 0) { + try { + proxyPort = Integer.parseInt(proxyHost.substring(i + 1)); + } catch (NumberFormatException ex) { + // ignore it + } + proxyHost = proxyHost.substring(0, i); + } + proxyPort = PropUtil.getIntProperty(props, + prefix + ".proxy.port", proxyPort); + err = "Using web proxy host, port: " + proxyHost + ", " + proxyPort; + if (logger.isLoggable(Level.FINER)) { + logger.finer("web proxy host " + proxyHost + ", port " + proxyPort); + if (proxyUser != null) + logger.finer("web proxy user " + proxyUser + ", password " + + (proxyPassword == null ? "" : "")); + } + } else if ((socksHost = + props.getProperty(prefix + ".socks.host", null)) != null) { + int i = socksHost.indexOf(':'); + if (i >= 0) { + try { + socksPort = Integer.parseInt(socksHost.substring(i + 1)); + } catch (NumberFormatException ex) { + // ignore it + } + socksHost = socksHost.substring(0, i); + } + socksPort = PropUtil.getIntProperty(props, + prefix + ".socks.port", socksPort); + err = "Using SOCKS host, port: " + socksHost + ", " + socksPort; + if (logger.isLoggable(Level.FINER)) + logger.finer("socks host " + socksHost + ", port " + socksPort); + } + + if (sf != null && !(sf instanceof SSLSocketFactory)) + socket = sf.createSocket(); + if (socket == null) { + if (socksHost != null) { + socket = new Socket( + new java.net.Proxy(java.net.Proxy.Type.SOCKS, + new InetSocketAddress(socksHost, socksPort))); + } else if (PropUtil.getBooleanProperty(props, + prefix + ".usesocketchannels", false)) { + logger.finer("using SocketChannels"); + socket = SocketChannel.open().socket(); + } else + socket = new Socket(); + } + if (to >= 0) { + if (logger.isLoggable(Level.FINEST)) + logger.finest("set socket read timeout " + to); + socket.setSoTimeout(to); + } + int writeTimeout = PropUtil.getIntProperty(props, + prefix + ".writetimeout", -1); + if (writeTimeout != -1) { // wrap original + if (logger.isLoggable(Level.FINEST)) + logger.finest("set socket write timeout " + writeTimeout); + socket = new WriteTimeoutSocket(socket, writeTimeout); + } + if (localaddr != null) + socket.bind(new InetSocketAddress(localaddr, localport)); + try { + logger.finest("connecting..."); + if (proxyHost != null) + proxyConnect(socket, proxyHost, proxyPort, + proxyUser, proxyPassword, host, port, cto); + else if (cto >= 0) + socket.connect(new InetSocketAddress(host, port), cto); + else + socket.connect(new InetSocketAddress(host, port)); + logger.finest("success!"); + } catch (IOException ex) { + logger.log(Level.FINEST, "connection failed", ex); + throw new SocketConnectException(err, ex, host, port, cto); + } + + /* + * If we want an SSL connection and we didn't get an SSLSocket, + * wrap our plain Socket with an SSLSocket. + */ + if ((useSSL || sf instanceof SSLSocketFactory) && + !(socket instanceof SSLSocket)) { + String trusted; + SSLSocketFactory ssf; + if ((trusted = props.getProperty(prefix + ".ssl.trust")) != null) { + try { + MailSSLSocketFactory msf = new MailSSLSocketFactory(); + if (trusted.equals("*")) + msf.setTrustAllHosts(true); + else + msf.setTrustedHosts(trusted.split("\\s+")); + ssf = msf; + } catch (GeneralSecurityException gex) { + IOException ioex = new IOException( + "Can't create MailSSLSocketFactory"); + ioex.initCause(gex); + throw ioex; + } + } else if (sf instanceof SSLSocketFactory) + ssf = (SSLSocketFactory)sf; + else + ssf = (SSLSocketFactory)SSLSocketFactory.getDefault(); + socket = ssf.createSocket(socket, host, port, true); + sf = ssf; + } + + /* + * No matter how we created the socket, if it turns out to be an + * SSLSocket, configure it. + */ + configureSSLSocket(socket, host, props, prefix, sf); + + return socket; + } + + /** + * Return a socket factory of the specified class. + */ + private static SocketFactory getSocketFactory(String sfClass) + throws ClassNotFoundException, + NoSuchMethodException, + IllegalAccessException, + InvocationTargetException { + if (sfClass == null || sfClass.length() == 0) + return null; + + // dynamically load the class + + ClassLoader cl = getContextClassLoader(); + Class clsSockFact = null; + if (cl != null) { + try { + clsSockFact = Class.forName(sfClass, false, cl); + } catch (ClassNotFoundException cex) { } + } + if (clsSockFact == null) + clsSockFact = Class.forName(sfClass); + // get & invoke the getDefault() method + Method mthGetDefault = clsSockFact.getMethod("getDefault", + new Class[]{}); + SocketFactory sf = (SocketFactory) + mthGetDefault.invoke(new Object(), new Object[]{}); + return sf; + } + + /** + * Start TLS on an existing socket. + * Supports the "STARTTLS" command in many protocols. + * This version for compatibility with possible third party code + * that might've used this API even though it shouldn't. + * + * @param socket the existing socket + * @return the wrapped Socket + * @exception IOException for I/O errors + * @deprecated + */ + @Deprecated + public static Socket startTLS(Socket socket) throws IOException { + return startTLS(socket, new Properties(), "socket"); + } + + /** + * Start TLS on an existing socket. + * Supports the "STARTTLS" command in many protocols. + * This version for compatibility with possible third party code + * that might've used this API even though it shouldn't. + * + * @param socket the existing socket + * @param props the properties + * @param prefix the property prefix + * @return the wrapped Socket + * @exception IOException for I/O errors + * @deprecated + */ + @Deprecated + public static Socket startTLS(Socket socket, Properties props, + String prefix) throws IOException { + InetAddress a = socket.getInetAddress(); + String host = a.getHostName(); + return startTLS(socket, host, props, prefix); + } + + /** + * Start TLS on an existing socket. + * Supports the "STARTTLS" command in many protocols. + * + * @param socket the existing socket + * @param host the host the socket is connected to + * @param props the properties + * @param prefix the property prefix + * @return the wrapped Socket + * @exception IOException for I/O errors + */ + public static Socket startTLS(Socket socket, String host, Properties props, + String prefix) throws IOException { + int port = socket.getPort(); + if (logger.isLoggable(Level.FINER)) + logger.finer("startTLS host " + host + ", port " + port); + + String sfErr = "unknown socket factory"; + try { + SSLSocketFactory ssf = null; + SocketFactory sf = null; + + // first, look for an SSL socket factory + Object sfo = props.get(prefix + ".ssl.socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "SSL socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".ssl.socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "SSL socket factory class " + sfClass; + } + if (sf != null && sf instanceof SSLSocketFactory) + ssf = (SSLSocketFactory)sf; + + // next, look for a regular socket factory that happens to be + // an SSL socket factory + if (ssf == null) { + sfo = props.get(prefix + ".socketFactory"); + if (sfo instanceof SocketFactory) { + sf = (SocketFactory)sfo; + sfErr = "socket factory instance " + sf; + } + if (sf == null) { + String sfClass = + props.getProperty(prefix + ".socketFactory.class"); + sf = getSocketFactory(sfClass); + sfErr = "socket factory class " + sfClass; + } + if (sf != null && sf instanceof SSLSocketFactory) + ssf = (SSLSocketFactory)sf; + } + + // finally, use the default SSL socket factory + if (ssf == null) { + String trusted; + if ((trusted = props.getProperty(prefix + ".ssl.trust")) != + null) { + try { + MailSSLSocketFactory msf = new MailSSLSocketFactory(); + if (trusted.equals("*")) + msf.setTrustAllHosts(true); + else + msf.setTrustedHosts(trusted.split("\\s+")); + ssf = msf; + sfErr = "mail SSL socket factory"; + } catch (GeneralSecurityException gex) { + IOException ioex = new IOException( + "Can't create MailSSLSocketFactory"); + ioex.initCause(gex); + throw ioex; + } + } else { + ssf = (SSLSocketFactory)SSLSocketFactory.getDefault(); + sfErr = "default SSL socket factory"; + } + } + + socket = ssf.createSocket(socket, host, port, true); + configureSSLSocket(socket, host, props, prefix, ssf); + } catch (Exception ex) { + if (ex instanceof InvocationTargetException) { + Throwable t = + ((InvocationTargetException)ex).getTargetException(); + if (t instanceof Exception) + ex = (Exception)t; + } + if (ex instanceof IOException) + throw (IOException)ex; + // wrap anything else before sending it on + IOException ioex = new IOException( + "Exception in startTLS using " + sfErr + + ": host, port: " + + host + ", " + port + + "; Exception: " + ex); + ioex.initCause(ex); + throw ioex; + } + return socket; + } + + /** + * Configure the SSL options for the socket (if it's an SSL socket), + * based on the mail..ssl.protocols and + * mail..ssl.ciphersuites properties. + * Check the identity of the server as specified by the + * mail..ssl.checkserveridentity property. + */ + private static void configureSSLSocket(Socket socket, String host, + Properties props, String prefix, SocketFactory sf) + throws IOException { + if (!(socket instanceof SSLSocket)) + return; + SSLSocket sslsocket = (SSLSocket)socket; + + String protocols = props.getProperty(prefix + ".ssl.protocols", null); + if (protocols != null) + sslsocket.setEnabledProtocols(stringArray(protocols)); + else { + /* + * The UW IMAP server insists on at least the TLSv1 + * protocol for STARTTLS, and won't accept the old SSLv2 + * or SSLv3 protocols. Here we enable only the non-SSL + * protocols. XXX - this should probably be parameterized. + */ + String[] prots = sslsocket.getEnabledProtocols(); + if (logger.isLoggable(Level.FINER)) + logger.finer("SSL enabled protocols before " + + Arrays.asList(prots)); + List eprots = new ArrayList<>(); + for (int i = 0; i < prots.length; i++) { + if (prots[i] != null && !prots[i].startsWith("SSL")) + eprots.add(prots[i]); + } + sslsocket.setEnabledProtocols( + eprots.toArray(new String[eprots.size()])); + } + String ciphers = props.getProperty(prefix + ".ssl.ciphersuites", null); + if (ciphers != null) + sslsocket.setEnabledCipherSuites(stringArray(ciphers)); + if (logger.isLoggable(Level.FINER)) { + logger.finer("SSL enabled protocols after " + + Arrays.asList(sslsocket.getEnabledProtocols())); + logger.finer("SSL enabled ciphers after " + + Arrays.asList(sslsocket.getEnabledCipherSuites())); + } + + /* + * Force the handshake to be done now so that we can report any + * errors (e.g., certificate errors) to the caller of the startTLS + * method. + */ + sslsocket.startHandshake(); + + /* + * Check server identity and trust. + */ + boolean idCheck = PropUtil.getBooleanProperty(props, + prefix + ".ssl.checkserveridentity", false); + if (idCheck) + checkServerIdentity(host, sslsocket); + if (sf instanceof MailSSLSocketFactory) { + MailSSLSocketFactory msf = (MailSSLSocketFactory)sf; + if (!msf.isServerTrusted(host, sslsocket)) { + throw cleanupAndThrow(sslsocket, + new IOException("Server is not trusted: " + host)); + } + } + } + + private static IOException cleanupAndThrow(Socket socket, IOException ife) { + try { + socket.close(); + } catch (Throwable thr) { + if (isRecoverable(thr)) { + ife.addSuppressed(thr); + } else { + thr.addSuppressed(ife); + if (thr instanceof Error) { + throw (Error) thr; + } + if (thr instanceof RuntimeException) { + throw (RuntimeException) thr; + } + throw new RuntimeException("unexpected exception", thr); + } + } + return ife; + } + + private static boolean isRecoverable(Throwable t) { + return (t instanceof Exception) || (t instanceof LinkageError); + } + + /** + * Check the server from the Socket connection against the server name(s) + * as expressed in the server certificate (RFC 2595 check). + * + * @param server name of the server expected + * @param sslSocket SSLSocket connected to the server + * @exception IOException if we can't verify identity of server + */ + private static void checkServerIdentity(String server, SSLSocket sslSocket) + throws IOException { + + // Check against the server name(s) as expressed in server certificate + try { + java.security.cert.Certificate[] certChain = + sslSocket.getSession().getPeerCertificates(); + if (certChain != null && certChain.length > 0 && + certChain[0] instanceof X509Certificate && + matchCert(server, (X509Certificate)certChain[0])) + return; + } catch (SSLPeerUnverifiedException e) { + sslSocket.close(); + IOException ioex = new IOException( + "Can't verify identity of server: " + server); + ioex.initCause(e); + throw ioex; + } + + // If we get here, there is nothing to consider the server as trusted. + sslSocket.close(); + throw new IOException("Can't verify identity of server: " + server); + } + + /** + * Do any of the names in the cert match the server name? + * + * @param server name of the server expected + * @param cert X509Certificate to get the subject's name from + * @return true if it matches + */ + private static boolean matchCert(String server, X509Certificate cert) { + if (logger.isLoggable(Level.FINER)) + logger.finer("matchCert server " + + server + ", cert " + cert); + + /* + * First, try to use sun.security.util.HostnameChecker, + * which exists in Sun's JDK starting with 1.4.1. + * We use reflection to access it in case it's not available + * in the JDK we're running on. + */ + try { + Class hnc = Class.forName("sun.security.util.HostnameChecker"); + // invoke HostnameChecker.getInstance(HostnameChecker.TYPE_LDAP) + // HostnameChecker.TYPE_LDAP == 2 + // LDAP requires the same regex handling as we need + Method getInstance = hnc.getMethod("getInstance", + new Class[] { byte.class }); + Object hostnameChecker = getInstance.invoke(new Object(), + new Object[] { Byte.valueOf((byte)2) }); + + // invoke hostnameChecker.match( server, cert) + if (logger.isLoggable(Level.FINER)) + logger.finer("using sun.security.util.HostnameChecker"); + Method match = hnc.getMethod("match", + new Class[] { String.class, X509Certificate.class }); + try { + match.invoke(hostnameChecker, new Object[] { server, cert }); + return true; + } catch (InvocationTargetException cex) { + logger.log(Level.FINER, "HostnameChecker FAIL", cex); + return false; + } + } catch (Exception ex) { + logger.log(Level.FINER, "NO sun.security.util.HostnameChecker", ex); + // ignore failure and continue below + } + + /* + * Lacking HostnameChecker, we implement a crude version of + * the same checks ourselves. + */ + try { + /* + * Check each of the subjectAltNames. + * XXX - only checks DNS names, should also handle + * case where server name is a literal IP address + */ + Collection> names = cert.getSubjectAlternativeNames(); + if (names != null) { + boolean foundName = false; + for (Iterator> it = names.iterator(); it.hasNext(); ) { + List nameEnt = it.next(); + Integer type = (Integer)nameEnt.get(0); + if (type.intValue() == 2) { // 2 == dNSName + foundName = true; + String name = (String)nameEnt.get(1); + if (logger.isLoggable(Level.FINER)) + logger.finer("found name: " + name); + if (matchServer(server, name)) + return true; + } + } + if (foundName) // found a name, but no match + return false; + } + } catch (CertificateParsingException ex) { + // ignore it + } + + // XXX - following is a *very* crude parse of the name and ignores + // all sorts of important issues such as quoting + Pattern p = Pattern.compile("CN=([^,]*)"); + Matcher m = p.matcher(cert.getSubjectX500Principal().getName()); + if (m.find() && matchServer(server, m.group(1).trim())) + return true; + + return false; + } + + /** + * Does the server we're expecting to connect to match the + * given name from the server's certificate? + * + * @param server name of the server expected + * @param name name from the server's certificate + */ + private static boolean matchServer(String server, String name) { + if (logger.isLoggable(Level.FINER)) + logger.finer("match server " + server + " with " + name); + if (name.startsWith("*.")) { + // match "foo.example.com" with "*.example.com" + String tail = name.substring(2); + if (tail.length() == 0) + return false; + int off = server.length() - tail.length(); + if (off < 1) + return false; + // if tail matches and is preceeded by "." + return server.charAt(off - 1) == '.' && + server.regionMatches(true, off, tail, 0, tail.length()); + } else + return server.equalsIgnoreCase(name); + } + + /** + * Use the HTTP CONNECT protocol to connect to a + * site through an HTTP proxy server.

+ * + * Protocol is roughly: + *

+     * CONNECT : HTTP/1.1
+     * Host: :
+     * 
+     *
+     * HTTP/1.1 200 Connect established
+     * 
+     * 
+     * 
+ */ + private static void proxyConnect(Socket socket, + String proxyHost, int proxyPort, + String proxyUser, String proxyPassword, + String host, int port, int cto) + throws IOException { + if (logger.isLoggable(Level.FINE)) + logger.fine("connecting through proxy " + + proxyHost + ":" + proxyPort + " to " + + host + ":" + port); + if (cto >= 0) + socket.connect(new InetSocketAddress(proxyHost, proxyPort), cto); + else + socket.connect(new InetSocketAddress(proxyHost, proxyPort)); + PrintStream os = new PrintStream(socket.getOutputStream(), false, + StandardCharsets.UTF_8.name()); + StringBuilder requestBuilder = new StringBuilder(); + requestBuilder.append("CONNECT ").append(host).append(":").append(port). + append(" HTTP/1.1\r\n"); + requestBuilder.append("Host: ").append(host).append(":").append(port). + append("\r\n"); + if (proxyUser != null && proxyPassword != null) { + byte[] upbytes = (proxyUser + ':' + proxyPassword). + getBytes(StandardCharsets.UTF_8); + String proxyHeaderValue = new String( + BASE64EncoderStream.encode(upbytes), + StandardCharsets.US_ASCII); + requestBuilder.append("Proxy-Authorization: Basic "). + append(proxyHeaderValue).append("\r\n"); + } + requestBuilder.append("Proxy-Connection: keep-alive\r\n\r\n"); + os.print(requestBuilder.toString()); + os.flush(); + BufferedReader r = new BufferedReader(new InputStreamReader( + socket.getInputStream(), StandardCharsets.UTF_8)); + String line; + boolean first = true; + while ((line = r.readLine()) != null) { + if (line.length() == 0) + break; + logger.finest(line); + if (first) { + StringTokenizer st = new StringTokenizer(line); + String http = st.nextToken(); + String code = st.nextToken(); + if (!code.equals("200")) { + try { + socket.close(); + } catch (IOException ioex) { + // ignored + } + ConnectException ex = new ConnectException( + "connection through proxy " + + proxyHost + ":" + proxyPort + " to " + + host + ":" + port + " failed: " + line); + logger.log(Level.FINE, "connect failed", ex); + throw ex; + } + first = false; + } + } + } + + /** + * Parse a string into whitespace separated tokens + * and return the tokens in an array. + */ + private static String[] stringArray(String s) { + StringTokenizer st = new StringTokenizer(s); + List tokens = new ArrayList<>(); + while (st.hasMoreTokens()) + tokens.add(st.nextToken()); + return tokens.toArray(new String[tokens.size()]); + } + + /** + * Convenience method to get our context class loader. + * Assert any privileges we might have and then call the + * Thread.getContextClassLoader method. + */ + private static ClassLoader getContextClassLoader() { + return + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public ClassLoader run() { + ClassLoader cl = null; + try { + cl = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ex) { } + return cl; + } + }); + } +} diff --git a/app/src/main/java/com/sun/mail/util/TraceInputStream.java b/app/src/main/java/com/sun/mail/util/TraceInputStream.java new file mode 100644 index 0000000000..fceb7ac45a --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/TraceInputStream.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.util.logging.Level; + +/** + * This class is a FilterInputStream that writes the bytes + * being read from the given input stream into the given output + * stream. This class is typically used to provide a trace of + * the data that is being retrieved from an input stream. + * + * @author John Mani + */ + +public class TraceInputStream extends FilterInputStream { + private boolean trace = false; + private boolean quote = false; + private OutputStream traceOut; + + /** + * Creates an input stream filter built on top of the specified + * input stream. + * + * @param in the underlying input stream. + * @param logger log trace here + */ + public TraceInputStream(InputStream in, MailLogger logger) { + super(in); + this.trace = logger.isLoggable(Level.FINEST); + this.traceOut = new LogOutputStream(logger); + } + + /** + * Creates an input stream filter built on top of the specified + * input stream. + * + * @param in the underlying input stream. + * @param traceOut the trace stream. + */ + public TraceInputStream(InputStream in, OutputStream traceOut) { + super(in); + this.traceOut = traceOut; + } + + /** + * Set trace mode. + * @param trace the trace mode + */ + public void setTrace(boolean trace) { + this.trace = trace; + } + + /** + * Set quote mode. + * @param quote the quote mode + */ + public void setQuote(boolean quote) { + this.quote = quote; + } + + /** + * Reads the next byte of data from this input stream. Returns + * -1 if no data is available. Writes out the read + * byte into the trace stream, if trace mode is true + */ + @Override + public int read() throws IOException { + int b = in.read(); + if (trace && b != -1) { + if (quote) + writeByte(b); + else + traceOut.write(b); + } + return b; + } + + /** + * Reads up to len bytes of data from this input stream + * into an array of bytes. Returns -1 if no more data + * is available. Writes out the read bytes into the trace stream, if + * trace mode is true + */ + @Override + public int read(byte b[], int off, int len) throws IOException { + int count = in.read(b, off, len); + if (trace && count != -1) { + if (quote) { + for (int i = 0; i < count; i++) + writeByte(b[off + i]); + } else + traceOut.write(b, off, count); + } + return count; + } + + /** + * Write a byte in a way that every byte value is printable ASCII. + */ + private final void writeByte(int b) throws IOException { + b &= 0xff; + if (b > 0x7f) { + traceOut.write('M'); + traceOut.write('-'); + b &= 0x7f; + } + if (b == '\r') { + traceOut.write('\\'); + traceOut.write('r'); + } else if (b == '\n') { + traceOut.write('\\'); + traceOut.write('n'); + traceOut.write('\n'); + } else if (b == '\t') { + traceOut.write('\\'); + traceOut.write('t'); + } else if (b < ' ') { + traceOut.write('^'); + traceOut.write('@' + b); + } else { + traceOut.write(b); + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/TraceOutputStream.java b/app/src/main/java/com/sun/mail/util/TraceOutputStream.java new file mode 100644 index 0000000000..28f282015a --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/TraceOutputStream.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.util.logging.Level; + +/** + * This class is a subclass of DataOutputStream that copies the + * data being written into the DataOutputStream into another output + * stream. This class is used here to provide a debug trace of the + * stuff thats being written out into the DataOutputStream. + * + * @author John Mani + */ + +public class TraceOutputStream extends FilterOutputStream { + private boolean trace = false; + private boolean quote = false; + private OutputStream traceOut; + + /** + * Creates an output stream filter built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream. + * @param logger log trace here + */ + public TraceOutputStream(OutputStream out, MailLogger logger) { + super(out); + this.trace = logger.isLoggable(Level.FINEST); + this.traceOut = new LogOutputStream(logger);; + } + + /** + * Creates an output stream filter built on top of the specified + * underlying output stream. + * + * @param out the underlying output stream. + * @param traceOut the trace stream. + */ + public TraceOutputStream(OutputStream out, OutputStream traceOut) { + super(out); + this.traceOut = traceOut; + } + + /** + * Set the trace mode. + * + * @param trace the trace mode + */ + public void setTrace(boolean trace) { + this.trace = trace; + } + + /** + * Set quote mode. + * @param quote the quote mode + */ + public void setQuote(boolean quote) { + this.quote = quote; + } + + /** + * Writes the specified byte to this output stream. + * Writes out the byte into the trace stream if the trace mode + * is true + * + * @param b the byte to write + * @exception IOException for I/O errors + */ + @Override + public void write(int b) throws IOException { + if (trace) { + if (quote) + writeByte(b); + else + traceOut.write(b); + } + out.write(b); + } + + /** + * Writes b.length bytes to this output stream. + * Writes out the bytes into the trace stream if the trace + * mode is true + * + * @param b bytes to write + * @param off offset in array + * @param len number of bytes to write + * @exception IOException for I/O errors + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + if (trace) { + if (quote) { + for (int i = 0; i < len; i++) + writeByte(b[off + i]); + } else + traceOut.write(b, off, len); + } + out.write(b, off, len); + } + + /** + * Write a byte in a way that every byte value is printable ASCII. + */ + private final void writeByte(int b) throws IOException { + b &= 0xff; + if (b > 0x7f) { + traceOut.write('M'); + traceOut.write('-'); + b &= 0x7f; + } + if (b == '\r') { + traceOut.write('\\'); + traceOut.write('r'); + } else if (b == '\n') { + traceOut.write('\\'); + traceOut.write('n'); + traceOut.write('\n'); + } else if (b == '\t') { + traceOut.write('\\'); + traceOut.write('t'); + } else if (b < ' ') { + traceOut.write('^'); + traceOut.write('@' + b); + } else { + traceOut.write(b); + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/UUDecoderStream.java b/app/src/main/java/com/sun/mail/util/UUDecoderStream.java new file mode 100644 index 0000000000..82f957cc0b --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/UUDecoderStream.java @@ -0,0 +1,334 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a UUDecoder. It is implemented as + * a FilterInputStream, so one can just wrap this class around + * any input stream and read bytes from this filter. The decoding + * is done as the bytes are read out. + * + * @author John Mani + * @author Bill Shannon + */ + +public class UUDecoderStream extends FilterInputStream { + private String name; + private int mode; + + private byte[] buffer = new byte[45]; // max decoded chars in a line = 45 + private int bufsize = 0; // size of the cache + private int index = 0; // index into the cache + private boolean gotPrefix = false; + private boolean gotEnd = false; + private LineInputStream lin; + private boolean ignoreErrors; + private boolean ignoreMissingBeginEnd; + private String readAhead; + + /** + * Create a UUdecoder that decodes the specified input stream. + * The System property mail.mime.uudecode.ignoreerrors + * controls whether errors in the encoded data cause an exception + * or are ignored. The default is false (errors cause exception). + * The System property mail.mime.uudecode.ignoremissingbeginend + * controls whether a missing begin or end line cause an exception + * or are ignored. The default is false (errors cause exception). + * @param in the input stream + */ + public UUDecoderStream(InputStream in) { + super(in); + lin = new LineInputStream(in); + // default to false + ignoreErrors = PropUtil.getBooleanSystemProperty( + "mail.mime.uudecode.ignoreerrors", false); + // default to false + ignoreMissingBeginEnd = PropUtil.getBooleanSystemProperty( + "mail.mime.uudecode.ignoremissingbeginend", false); + } + + /** + * Create a UUdecoder that decodes the specified input stream. + * @param in the input stream + * @param ignoreErrors ignore errors? + * @param ignoreMissingBeginEnd ignore missing begin or end? + */ + public UUDecoderStream(InputStream in, boolean ignoreErrors, + boolean ignoreMissingBeginEnd) { + super(in); + lin = new LineInputStream(in); + this.ignoreErrors = ignoreErrors; + this.ignoreMissingBeginEnd = ignoreMissingBeginEnd; + } + + /** + * Read the next decoded byte from this input stream. The byte + * is returned as an int in the range 0 + * to 255. If no byte is available because the end of + * the stream has been reached, the value -1 is returned. + * This method blocks until input data is available, the end of the + * stream is detected, or an exception is thrown. + * + * @return next byte of data, or -1 if the end of + * stream is reached. + * @exception IOException if an I/O error occurs. + * @see java.io.FilterInputStream#in + */ + @Override + public int read() throws IOException { + if (index >= bufsize) { + readPrefix(); + if (!decode()) + return -1; + index = 0; // reset index into buffer + } + return buffer[index++] & 0xff; // return lower byte + } + + @Override + public int read(byte[] buf, int off, int len) throws IOException { + int i, c; + for (i = 0; i < len; i++) { + if ((c = read()) == -1) { + if (i == 0) // At end of stream, so we should + i = -1; // return -1, NOT 0. + break; + } + buf[off+i] = (byte)c; + } + return i; + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public int available() throws IOException { + // This is only an estimate, since in.available() + // might include CRLFs too .. + return ((in.available() * 3)/4 + (bufsize-index)); + } + + /** + * Get the "name" field from the prefix. This is meant to + * be the pathname of the decoded file + * + * @return name of decoded file + * @exception IOException if an I/O error occurs. + */ + public String getName() throws IOException { + readPrefix(); + return name; + } + + /** + * Get the "mode" field from the prefix. This is the permission + * mode of the source file. + * + * @return permission mode of source file + * @exception IOException if an I/O error occurs. + */ + public int getMode() throws IOException { + readPrefix(); + return mode; + } + + /** + * UUencoded streams start off with the line: + * "begin " + * Search for this prefix and gobble it up. + */ + private void readPrefix() throws IOException { + if (gotPrefix) // got the prefix + return; + + mode = 0666; // defaults, overridden below + name = "encoder.buf"; // same default used by encoder + String line; + for (;;) { + // read till we get the prefix: "begin MODE FILENAME" + line = lin.readLine(); // NOTE: readLine consumes CRLF pairs too + if (line == null) { + if (!ignoreMissingBeginEnd) + throw new DecodingException("UUDecoder: Missing begin"); + // at EOF, fake it + gotPrefix = true; + gotEnd = true; + break; + } + if (line.regionMatches(false, 0, "begin", 0, 5)) { + try { + mode = Integer.parseInt(line.substring(6,9)); + } catch (NumberFormatException ex) { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Error in mode: " + ex.toString()); + } + if (line.length() > 10) { + name = line.substring(10); + } else { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Missing name: " + line); + } + gotPrefix = true; + break; + } else if (ignoreMissingBeginEnd && line.length() != 0) { + int count = line.charAt(0); + count = (count - ' ') & 0x3f; + int need = ((count * 8)+5)/6; + if (need == 0 || line.length() >= need + 1) { + /* + * Looks like a legitimate encoded line. + * Pretend we saw the "begin" line and + * save this line for later processing in + * decode(). + */ + readAhead = line; + gotPrefix = true; // fake it + break; + } + } + } + } + + private boolean decode() throws IOException { + + if (gotEnd) + return false; + bufsize = 0; + int count = 0; + String line; + for (;;) { + /* + * If we ignored a missing "begin", the first line + * will be saved in readAhead. + */ + if (readAhead != null) { + line = readAhead; + readAhead = null; + } else + line = lin.readLine(); + + /* + * Improperly encoded data sometimes omits the zero length + * line that starts with a space character, we detect the + * following "end" line here. + */ + if (line == null) { + if (!ignoreMissingBeginEnd) + throw new DecodingException( + "UUDecoder: Missing end at EOF"); + gotEnd = true; + return false; + } + if (line.equals("end")) { + gotEnd = true; + return false; + } + if (line.length() == 0) + continue; + count = line.charAt(0); + if (count < ' ') { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Buffer format error"); + continue; + } + + /* + * The first character in a line is the number of original (not + * the encoded atoms) characters in the line. Note that all the + * code below has to handle the character that indicates + * end of encoded stream. + */ + count = (count - ' ') & 0x3f; + + if (count == 0) { + line = lin.readLine(); + if (line == null || !line.equals("end")) { + if (!ignoreMissingBeginEnd) + throw new DecodingException( + "UUDecoder: Missing End after count 0 line"); + } + gotEnd = true; + return false; + } + + int need = ((count * 8)+5)/6; +//System.out.println("count " + count + ", need " + need + ", len " + line.length()); + if (line.length() < need + 1) { + if (!ignoreErrors) + throw new DecodingException( + "UUDecoder: Short buffer error"); + continue; + } + + // got a line we're committed to, break out and decode it + break; + } + + int i = 1; + byte a, b; + /* + * A correct uuencoder always encodes 3 characters at a time, even + * if there aren't 3 characters left. But since some people out + * there have broken uuencoders we handle the case where they + * don't include these "unnecessary" characters. + */ + while (bufsize < count) { + // continue decoding until we get 'count' decoded chars + a = (byte)((line.charAt(i++) - ' ') & 0x3f); + b = (byte)((line.charAt(i++) - ' ') & 0x3f); + buffer[bufsize++] = (byte)(((a << 2) & 0xfc) | ((b >>> 4) & 3)); + + if (bufsize < count) { + a = b; + b = (byte)((line.charAt(i++) - ' ') & 0x3f); + buffer[bufsize++] = + (byte)(((a << 4) & 0xf0) | ((b >>> 2) & 0xf)); + } + + if (bufsize < count) { + a = b; + b = (byte)((line.charAt(i++) - ' ') & 0x3f); + buffer[bufsize++] = (byte)(((a << 6) & 0xc0) | (b & 0x3f)); + } + } + return true; + } + + /*** begin TEST program ***** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + UUDecoderStream decoder = new UUDecoderStream(infile); + int c; + + try { + while ((c = decoder.read()) != -1) + System.out.write(c); + System.out.flush(); + } catch (Exception e) { + e.printStackTrace(); + } + } + **** end TEST program ****/ +} diff --git a/app/src/main/java/com/sun/mail/util/UUEncoderStream.java b/app/src/main/java/com/sun/mail/util/UUEncoderStream.java new file mode 100644 index 0000000000..183cc34737 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/UUEncoderStream.java @@ -0,0 +1,205 @@ +/* + * Copyright (c) 1997, 2018 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; + +/** + * This class implements a UUEncoder. It is implemented as + * a FilterOutputStream, so one can just wrap this class around + * any output stream and write bytes into this filter. The Encoding + * is done as the bytes are written out. + * + * @author John Mani + */ + +public class UUEncoderStream extends FilterOutputStream { + private byte[] buffer; // cache of bytes that are yet to be encoded + private int bufsize = 0; // size of the cache + private boolean wrotePrefix = false; + private boolean wroteSuffix = false; + + private String name; // name of file + private int mode; // permissions mode + + /** + * Create a UUencoder that encodes the specified input stream + * @param out the output stream + */ + public UUEncoderStream(OutputStream out) { + this(out, "encoder.buf", 0644); + } + + /** + * Create a UUencoder that encodes the specified input stream + * @param out the output stream + * @param name Specifies a name for the encoded buffer + */ + public UUEncoderStream(OutputStream out, String name) { + this(out, name, 0644); + } + + /** + * Create a UUencoder that encodes the specified input stream + * @param out the output stream + * @param name Specifies a name for the encoded buffer + * @param mode Specifies permission mode for the encoded buffer + */ + public UUEncoderStream(OutputStream out, String name, int mode) { + super(out); + this.name = name; + this.mode = mode; + buffer = new byte[45]; + } + + /** + * Set up the buffer name and permission mode. + * This method has any effect only if it is invoked before + * you start writing into the output stream + * + * @param name the buffer name + * @param mode the permission mode + */ + public void setNameMode(String name, int mode) { + this.name = name; + this.mode = mode; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + for (int i = 0; i < len; i++) + write(b[off + i]); + } + + @Override + public void write(byte[] data) throws IOException { + write(data, 0, data.length); + } + + @Override + public void write(int c) throws IOException { + /* buffer up characters till we get a line's worth, then encode + * and write them out. Max number of characters allowed per + * line is 45. + */ + buffer[bufsize++] = (byte)c; + if (bufsize == 45) { + writePrefix(); + encode(); + bufsize = 0; + } + } + + @Override + public void flush() throws IOException { + if (bufsize > 0) { // If there's unencoded characters in the buffer + writePrefix(); + encode(); // .. encode them + bufsize = 0; + } + writeSuffix(); + out.flush(); + } + + @Override + public void close() throws IOException { + flush(); + out.close(); + } + + /** + * Write out the prefix: "begin " + */ + private void writePrefix() throws IOException { + if (!wrotePrefix) { + // name should be ASCII, but who knows... + PrintStream ps = new PrintStream(out, false, "utf-8"); + ps.format("begin %o %s%n", mode, name); + ps.flush(); + wrotePrefix = true; + } + } + + /** + * Write a single line containing space and the suffix line + * containing the single word "end" (terminated by a newline) + */ + private void writeSuffix() throws IOException { + if (!wroteSuffix) { + PrintStream ps = new PrintStream(out, false, "us-ascii"); + ps.println(" \nend"); + ps.flush(); + wroteSuffix = true; + } + } + + /** + * Encode a line. + * Start off with the character count, followed by the encoded atoms + * and terminate with LF. (or is it CRLF or the local line-terminator ?) + * Take three bytes and encodes them into 4 characters + * If bufsize if not a multiple of 3, the remaining bytes are filled + * with '1'. This insures that the last line won't end in spaces + * and potentiallly be truncated. + */ + private void encode() throws IOException { + byte a, b, c; + int c1, c2, c3, c4; + int i = 0; + + // Start off with the count of characters in the line + out.write((bufsize & 0x3f) + ' '); + + while (i < bufsize) { + a = buffer[i++]; + if (i < bufsize) { + b = buffer[i++]; + if (i < bufsize) + c = buffer[i++]; + else // default c to 1 + c = 1; + } + else { // default b & c to 1 + b = 1; + c = 1; + } + + c1 = (a >>> 2) & 0x3f; + c2 = ((a << 4) & 0x30) | ((b >>> 4) & 0xf); + c3 = ((b << 2) & 0x3c) | ((c >>> 6) & 0x3); + c4 = c & 0x3f; + out.write(c1 + ' '); + out.write(c2 + ' '); + out.write(c3 + ' '); + out.write(c4 + ' '); + } + // Terminate with LF. (should it be CRLF or local line-terminator ?) + out.write('\n'); + } + + /**** begin TEST program ***** + public static void main(String argv[]) throws Exception { + FileInputStream infile = new FileInputStream(argv[0]); + UUEncoderStream encoder = new UUEncoderStream(System.out); + int c; + + while ((c = infile.read()) != -1) + encoder.write(c); + encoder.close(); + } + **** end TEST program *****/ +} diff --git a/app/src/main/java/com/sun/mail/util/WriteTimeoutSocket.java b/app/src/main/java/com/sun/mail/util/WriteTimeoutSocket.java new file mode 100644 index 0000000000..1c84b52985 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/WriteTimeoutSocket.java @@ -0,0 +1,401 @@ +/* + * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util; + +import java.io.*; +import java.net.*; +import java.util.concurrent.*; +import java.util.Collections; +import java.util.Set; +import java.nio.channels.SocketChannel; +import java.lang.reflect.*; + +/** + * A special Socket that uses a ScheduledExecutorService to + * implement timeouts for writes. The write timeout is specified + * (in milliseconds) when the WriteTimeoutSocket is created. + * + * @author Bill Shannon + */ +public class WriteTimeoutSocket extends Socket { + + // delegate all operations to this socket + private final Socket socket; + // to schedule task to cancel write after timeout + private final ScheduledExecutorService ses; + // the timeout, in milliseconds + private final int timeout; + + public WriteTimeoutSocket(Socket socket, int timeout) throws IOException { + this.socket = socket; + // XXX - could share executor with all instances? + this.ses = Executors.newScheduledThreadPool(1); + this.timeout = timeout; + } + + public WriteTimeoutSocket(int timeout) throws IOException { + this(new Socket(), timeout); + } + + public WriteTimeoutSocket(InetAddress address, int port, int timeout) + throws IOException { + this(timeout); + socket.connect(new InetSocketAddress(address, port)); + } + + public WriteTimeoutSocket(InetAddress address, int port, + InetAddress localAddress, int localPort, int timeout) + throws IOException { + this(timeout); + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(address, port)); + } + + public WriteTimeoutSocket(String host, int port, int timeout) + throws IOException { + this(timeout); + socket.connect(new InetSocketAddress(host, port)); + } + + public WriteTimeoutSocket(String host, int port, + InetAddress localAddress, int localPort, int timeout) + throws IOException { + this(timeout); + socket.bind(new InetSocketAddress(localAddress, localPort)); + socket.connect(new InetSocketAddress(host, port)); + } + + // override all Socket methods and delegate to underlying Socket + + @Override + public void connect(SocketAddress remote) throws IOException { + socket.connect(remote, 0); + } + + @Override + public void connect(SocketAddress remote, int timeout) throws IOException { + socket.connect(remote, timeout); + } + + @Override + public void bind(SocketAddress local) throws IOException { + socket.bind(local); + } + + @Override + public SocketAddress getRemoteSocketAddress() { + return socket.getRemoteSocketAddress(); + } + + @Override + public SocketAddress getLocalSocketAddress() { + return socket.getLocalSocketAddress(); + } + + @Override + public void setPerformancePreferences(int connectionTime, int latency, + int bandwidth) { + socket.setPerformancePreferences(connectionTime, latency, bandwidth); + } + + @Override + public SocketChannel getChannel() { + return socket.getChannel(); + } + + @Override + public InetAddress getInetAddress() { + return socket.getInetAddress(); + } + + @Override + public InetAddress getLocalAddress() { + return socket.getLocalAddress(); + } + + @Override + public int getPort() { + return socket.getPort(); + } + + @Override + public int getLocalPort() { + return socket.getLocalPort(); + } + + @Override + public InputStream getInputStream() throws IOException { + return socket.getInputStream(); + } + + @Override + public synchronized OutputStream getOutputStream() throws IOException { + // wrap the returned stream to implement write timeout + return new TimeoutOutputStream(socket.getOutputStream(), ses, timeout); + } + + @Override + public void setTcpNoDelay(boolean on) throws SocketException { + socket.setTcpNoDelay(on); + } + + @Override + public boolean getTcpNoDelay() throws SocketException { + return socket.getTcpNoDelay(); + } + + @Override + public void setSoLinger(boolean on, int linger) throws SocketException { + socket.setSoLinger(on, linger); + } + + @Override + public int getSoLinger() throws SocketException { + return socket.getSoLinger(); + } + + @Override + public void sendUrgentData(int data) throws IOException { + socket.sendUrgentData(data); + } + + @Override + public void setOOBInline(boolean on) throws SocketException { + socket.setOOBInline(on); + } + + @Override + public boolean getOOBInline() throws SocketException { + return socket.getOOBInline(); + } + + @Override + public void setSoTimeout(int timeout) throws SocketException { + socket.setSoTimeout(timeout); + } + + @Override + public int getSoTimeout() throws SocketException { + return socket.getSoTimeout(); + } + + @Override + public void setSendBufferSize(int size) throws SocketException { + socket.setSendBufferSize(size); + } + + @Override + public int getSendBufferSize() throws SocketException { + return socket.getSendBufferSize(); + } + + @Override + public void setReceiveBufferSize(int size) throws SocketException { + socket.setReceiveBufferSize(size); + } + + @Override + public int getReceiveBufferSize() throws SocketException { + return socket.getReceiveBufferSize(); + } + + @Override + public void setKeepAlive(boolean on) throws SocketException { + socket.setKeepAlive(on); + } + + @Override + public boolean getKeepAlive() throws SocketException { + return socket.getKeepAlive(); + } + + @Override + public void setTrafficClass(int tc) throws SocketException { + socket.setTrafficClass(tc); + } + + @Override + public int getTrafficClass() throws SocketException { + return socket.getTrafficClass(); + } + + @Override + public void setReuseAddress(boolean on) throws SocketException { + socket.setReuseAddress(on); + } + + @Override + public boolean getReuseAddress() throws SocketException { + return socket.getReuseAddress(); + } + + @Override + public void close() throws IOException { + try { + socket.close(); + } finally { + ses.shutdownNow(); + } + } + + @Override + public void shutdownInput() throws IOException { + socket.shutdownInput(); + } + + @Override + public void shutdownOutput() throws IOException { + socket.shutdownOutput(); + } + + @Override + public String toString() { + return socket.toString(); + } + + @Override + public boolean isConnected() { + return socket.isConnected(); + } + + @Override + public boolean isBound() { + return socket.isBound(); + } + + @Override + public boolean isClosed() { + return socket.isClosed(); + } + + @Override + public boolean isInputShutdown() { + return socket.isInputShutdown(); + } + + @Override + public boolean isOutputShutdown() { + return socket.isOutputShutdown(); + } + + /* + * The following three methods were added to java.net.Socket in Java SE 9. + * Since they're not supported on Android, and since we know that we + * never use them in Jakarta Mail, we just stub them out here. + */ + //@Override + public Socket setOption(SocketOption so, T val) throws IOException { + // socket.setOption(so, val); + // return this; + throw new UnsupportedOperationException("WriteTimeoutSocket.setOption"); + } + + //@Override + public T getOption(SocketOption so) throws IOException { + // return socket.getOption(so); + throw new UnsupportedOperationException("WriteTimeoutSocket.getOption"); + } + + //@Override + public Set> supportedOptions() { + // return socket.supportedOptions(); + return Collections.emptySet(); + } + + /** + * KLUDGE for Android, which has this illegal non-Java Compatible method. + * + * @return the FileDescriptor object + */ + public FileDescriptor getFileDescriptor$() { + try { + Method m = Socket.class.getDeclaredMethod("getFileDescriptor$"); + return (FileDescriptor)m.invoke(socket); + } catch (Exception ex) { + return null; + } + } +} + + +/** + * An OutputStream that wraps the Socket's OutputStream and uses + * the ScheduledExecutorService to schedule a task to close the + * socket (aborting the write) if the timeout expires. + */ +class TimeoutOutputStream extends OutputStream { + private final OutputStream os; + private final ScheduledExecutorService ses; + private final Callable timeoutTask; + private final int timeout; + private byte[] b1; + + public TimeoutOutputStream(OutputStream os0, ScheduledExecutorService ses, + int timeout) throws IOException { + this.os = os0; + this.ses = ses; + this.timeout = timeout; + timeoutTask = new Callable() { + @Override + public Object call() throws Exception { + os.close(); // close the stream to abort the write + return null; + } + }; + } + + @Override + public synchronized void write(int b) throws IOException { + if (b1 == null) + b1 = new byte[1]; + b1[0] = (byte)b; + this.write(b1); + } + + @Override + public synchronized void write(byte[] bs, int off, int len) + throws IOException { + if ((off < 0) || (off > bs.length) || (len < 0) || + ((off + len) > bs.length) || ((off + len) < 0)) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return; + } + + // Implement timeout with a scheduled task + ScheduledFuture sf = null; + try { + try { + if (timeout > 0) + sf = ses.schedule(timeoutTask, + timeout, TimeUnit.MILLISECONDS); + } catch (RejectedExecutionException ex) { + // ignore it; Executor was shut down by another thread, + // the following write should fail with IOException + } + os.write(bs, off, len); + } finally { + if (sf != null) + sf.cancel(true); + } + } + + @Override + public void close() throws IOException { + os.close(); + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/CollectorFormatter.java b/app/src/main/java/com/sun/mail/util/logging/CollectorFormatter.java new file mode 100644 index 0000000000..d2d889c215 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/CollectorFormatter.java @@ -0,0 +1,603 @@ +/* + * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2019 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import static com.sun.mail.util.logging.LogManagerProperties.fromLogManager; +import java.lang.reflect.UndeclaredThrowableException; +import java.text.MessageFormat; +import java.util.Comparator; +import java.util.Locale; +import java.util.ResourceBundle; +import java.util.logging.Formatter; +import java.util.logging.Handler; +import java.util.logging.LogRecord; + +/** + * A LogRecord formatter that takes a sequence of LogRecords and combines them + * into a single summary result. Formating of the head, LogRecord, and tail are + * delegated to the wrapped formatter. + * + *

+ * By default each CollectorFormatter is initialized using the + * following LogManager configuration properties where + * <formatter-name> refers to the fully qualified class name + * or the fully qualified derived class name of the formatter. If properties + * are not defined, or contain invalid values, then the specified default values + * are used. + *

    + *
  • <formatter-name>.comparator name of a + * {@linkplain java.util.Comparator} class used to choose the collected + * LogRecord. If a comparator is specified then the max + * LogRecord is chosen. If comparator is set to the string literal + * null, then the last record is chosen. (defaults to + * {@linkplain SeverityComparator}) + * + *
  • <formatter-name>.comparator.reverse a boolean + * true to collect the min LogRecord or + * false to collect the max LogRecord. + * (defaults to false) + * + *
  • <formatter-name>.format the + * {@linkplain java.text.MessageFormat MessageFormat} string used to format the + * collected summary statistics. The arguments are explained in detail in the + * {@linkplain #getTail(java.util.logging.Handler) getTail} documentation. + * (defaults to {0}{1}{2}{4,choice,-1#|0#|0<... {4,number,integer} + * more}\n) + * + *
  • <formatter-name>.formatter name of a Formatter class + * used to format the collected LogRecord. + * (defaults to {@linkplain CompactFormatter}) + * + *
+ * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class CollectorFormatter extends Formatter { + /** + * Avoid depending on JMX runtime bean to get the start time. + */ + private static final long INIT_TIME = System.currentTimeMillis(); + /** + * The message format string used as the formatted output. + */ + private final String fmt; + /** + * The formatter used to format the chosen log record. + */ + private final Formatter formatter; + /** + * The comparator used to pick the log record to format. + */ + private final Comparator comparator; + /** + * The last accepted record. Synchronized access is preferred over volatile + * for this class. + */ + private LogRecord last; + /** + * The number of log records that have been formatted. + */ + private long count; + /** + * The number of log produced containing at least one log record. + * Only incremented when this formatter is reset. + */ + private long generation = 1L; + /** + * The number of log records that have been formatted with a thrown object. + */ + private long thrown; + /** + * The eldest log record time or eldest time possible for this instance. + */ + private long minMillis = INIT_TIME; + /** + * The newest log record time. + */ + private long maxMillis = Long.MIN_VALUE; + + /** + * Creates the formatter using the LogManager defaults. + * + * @throws SecurityException if a security manager exists and the caller + * does not have LoggingPermission("control"). + * @throws UndeclaredThrowableException if there are problems loading from + * the LogManager. + */ + public CollectorFormatter() { + final String p = getClass().getName(); + this.fmt = initFormat(p); + this.formatter = initFormatter(p); + this.comparator = initComparator(p); + } + + /** + * Creates the formatter using the given format. + * + * @param format the message format or null to use the LogManager default. + * @throws SecurityException if a security manager exists and the caller + * does not have LoggingPermission("control"). + * @throws UndeclaredThrowableException if there are problems loading from + * the LogManager. + */ + public CollectorFormatter(String format) { + final String p = getClass().getName(); + this.fmt = format == null ? initFormat(p) : format; + this.formatter = initFormatter(p); + this.comparator = initComparator(p); + } + + /** + * Creates the formatter using the given values. + * + * @param format the format string or null to use the LogManager default. + * @param f the formatter used on the collected log record or null to + * specify no formatter. + * @param c the comparator used to determine which log record to format or + * null to specify no comparator. + * @throws SecurityException if a security manager exists and the caller + * does not have LoggingPermission("control"). + * @throws UndeclaredThrowableException if there are problems loading from + * the LogManager. + */ + public CollectorFormatter(String format, Formatter f, + Comparator c) { + final String p = getClass().getName(); + this.fmt = format == null ? initFormat(p) : format; + this.formatter = f; + this.comparator = c; + } + + /** + * Accumulates log records which will be used to produce the final output. + * The output is generated using the {@link #getTail} method which also + * resets this formatter back to its original state. + * + * @param record the record to store. + * @return an empty string. + * @throws NullPointerException if the given record is null. + */ + @Override + public String format(final LogRecord record) { + if (record == null) { + throw new NullPointerException(); + } + + boolean accepted; + do { + final LogRecord peek = peek(); + //The self compare of the first record acts like a type check. + LogRecord update = apply(peek != null ? peek : record, record); + if (peek != update) { //Not identical. + update.getSourceMethodName(); //Infer caller, null check. + accepted = acceptAndUpdate(peek, update); + } else { + accepted = accept(peek, record); + } + } while (!accepted); + return ""; + } + + /** + * Formats the collected LogRecord and summary statistics. The collected + * results are reset after calling this method. The + * {@linkplain java.text.MessageFormat java.text} argument indexes are + * assigned to the following properties: + * + *
    + *
  1. {@code head} the + * {@linkplain Formatter#getHead(java.util.logging.Handler) head} string + * returned from the target formatter and + * {@linkplain #finish(java.lang.String) finished} by this formatter. + *
  2. {@code formatted} the current log record + * {@linkplain Formatter#format(java.util.logging.LogRecord) formatted} by + * the target formatter and {@linkplain #finish(java.lang.String) finished} + * by this formatter. If the formatter is null then record is formatted by + * this {@linkplain #formatMessage(java.util.logging.LogRecord) formatter}. + *
  3. {@code tail} the + * {@linkplain Formatter#getTail(java.util.logging.Handler) tail} string + * returned from the target formatter and + * {@linkplain #finish(java.lang.String) finished} by this formatter. + *
  4. {@code count} the total number of log records + * {@linkplain #format consumed} by this formatter. + *
  5. {@code remaining} the count minus one. + *
  6. {@code thrown} the total number of log records + * {@linkplain #format consumed} by this formatter with an assigned + * {@linkplain java.util.logging.LogRecord#getThrown throwable}. + *
  7. {@code normal messages} the count minus the thrown. + *
  8. {@code minMillis} the eldest log record + * {@linkplain java.util.logging.LogRecord#getMillis event time} + * {@linkplain #format consumed} by this formatter. If the count is zero + * then this is set to the previous max or approximate start time if there + * was no previous max. By default this parameter is defined as a number. + * The format type and format style rules from the + * {@linkplain java.text.MessageFormat} should be used to convert this from + * milliseconds to a date or time. + *
  9. {@code maxMillis} the most recent log record + * {@linkplain java.util.logging.LogRecord#getMillis event time} + * {@linkplain #format consumed} by this formatter. If the count is zero + * then this is set to the {@linkplain System#currentTimeMillis() current time}. + * By default this parameter is defined as a number. The format type and + * format style rules from the {@linkplain java.text.MessageFormat} should + * be used to convert this from milliseconds to a date or time. + *
  10. {@code elapsed} the elapsed time in milliseconds between the + * {@code maxMillis} and {@code minMillis}. + *
  11. {@code startTime} the approximate start time in milliseconds. By + * default this parameter is defined as a number. The format type and format + * style rules from the {@linkplain java.text.MessageFormat} should be used + * to convert this from milliseconds to a date or time. + *
  12. {@code currentTime} the + * {@linkplain System#currentTimeMillis() current time} in milliseconds. By + * default this parameter is defined as a number. The format type and format + * style rules from the {@linkplain java.text.MessageFormat} should be used + * to convert this from milliseconds to a date or time. + *
  13. {@code uptime} the elapsed time in milliseconds between the + * {@code currentTime} and {@code startTime}. + *
  14. {@code generation} the number times this method produced output with + * at least one {@linkplain #format consumed} log record. This can be used + * to track the number of complete reports this formatter has produced. + *
+ * + *

+ * Some example formats:
+ *

    + *
  • {@code com.sun.mail.util.logging.CollectorFormatter.format={0}{1}{2}{4,choice,-1#|0#|0<... {4,number,integer} more}\n} + *

    + * This prints the head ({@code {0}}), format ({@code {1}}), and tail + * ({@code {2}}) from the target formatter followed by the number of + * remaining ({@code {4}}) log records consumed by this formatter if there + * are any remaining records. + *

    +     * Encoding failed.|NullPointerException: null String.getBytes(:913)... 3 more
    +     * 
    + *
  • {@code com.sun.mail.util.logging.CollectorFormatter.format=These {3} messages occurred between\n{7,date,EEE, MMM dd HH:mm:ss:S ZZZ yyyy} and {8,time,EEE, MMM dd HH:mm:ss:S ZZZ yyyy}\n} + *

    + * This prints the count ({@code {3}}) followed by the date and time of the + * eldest log record ({@code {7}}) and the date and time of the most recent + * log record ({@code {8}}). + *

    +     * These 292 messages occurred between
    +     * Tue, Jul 21 14:11:42:449 -0500 2009 and Fri, Nov 20 07:29:24:0 -0600 2009
    +     * 
    + *
  • {@code com.sun.mail.util.logging.CollectorFormatter.format=These {3} messages occurred between {9,choice,86400000#{7,date} {7,time} and {8,time}|86400000<{7,date} and {8,date}}\n} + *

    + * This prints the count ({@code {3}}) and then chooses the format based on + * the elapsed time ({@code {9}}). If the elapsed time is less than one day + * then the eldest log record ({@code {7}}) date and time is formatted + * followed by just the time of the most recent log record ({@code {8}}. + * Otherwise, the just the date of the eldest log record ({@code {7}}) and + * just the date of most recent log record ({@code {8}} is formatted. + *

    +     * These 73 messages occurred between Jul 21, 2009 2:11:42 PM and 2:13:32 PM
    +     *
    +     * These 116 messages occurred between Jul 21, 2009 and Aug 20, 2009
    +     * 
    + *
  • {@code com.sun.mail.util.logging.CollectorFormatter.format={13} alert reports since {10,date}.\n} + *

    + * This prints the generation ({@code {13}}) followed by the start time + * ({@code {10}}) formatted as a date. + *

    +     * 4,320 alert reports since Jul 21, 2012.
    +     * 
    + *
+ * + * @param h the handler or null. + * @return the output string. + */ + @Override + public String getTail(final Handler h) { + super.getTail(h); //Be forward compatible with super.getHead. + return formatRecord(h, true); + } + + /** + * Formats the collected LogRecord and summary statistics. The LogRecord and + * summary statistics are not changed by calling this method. + * + * @return the current record formatted or the default toString. + * @see #getTail(java.util.logging.Handler) + */ + @Override + public String toString() { + String result; + try { + result = formatRecord((Handler) null, false); + } catch (final RuntimeException ignore) { + result = super.toString(); + } + return result; + } + + /** + * Used to choose the collected LogRecord. This implementation returns the + * greater of two LogRecords. + * + * @param t the current record. + * @param u the record that could replace the current. + * @return the greater of the given log records. + * @throws NullPointerException may occur if either record is null. + */ + protected LogRecord apply(final LogRecord t, final LogRecord u) { + if (t == null || u == null) { + throw new NullPointerException(); + } + + if (comparator != null) { + return comparator.compare(t, u) >= 0 ? t : u; + } else { + return u; + } + } + + /** + * Updates the summary statistics only if the expected record matches the + * last record. The update record is not stored. + * + * @param e the LogRecord that is expected. + * @param u the LogRecord used to collect statistics. + * @return true if the last record was the expected record. + * @throws NullPointerException if the update record is null. + */ + private synchronized boolean accept(final LogRecord e, final LogRecord u) { + /** + * LogRecord methods must be called before the check of the last stored + * record to guard against subclasses of LogRecord that might attempt to + * reset the state by triggering a call to getTail. + */ + final long millis = u.getMillis(); //Null check. + final Throwable ex = u.getThrown(); + if (last == e) { //Only if the exact same reference. + if (++count != 1L) { + minMillis = Math.min(minMillis, millis); + } else { //Show single records as instant and not a time period. + minMillis = millis; + } + maxMillis = Math.max(maxMillis, millis); + + if (ex != null) { + ++thrown; + } + return true; + } else { + return false; + } + } + + /** + * Resets all of the collected summary statistics including the LogRecord. + * @param min the current min milliseconds. + */ + private synchronized void reset(final long min) { + if (last != null) { + last = null; + ++generation; + } + + count = 0L; + thrown = 0L; + minMillis = min; + maxMillis = Long.MIN_VALUE; + } + + /** + * Formats the given record with the head and tail. + * + * @param h the Handler or null. + * @param reset true if the summary statistics and LogRecord should be reset + * back to initial values. + * @return the formatted string. + * @see #getTail(java.util.logging.Handler) + */ + private String formatRecord(final Handler h, final boolean reset) { + final LogRecord record; + final long c; + final long t; + final long g; + long msl; + long msh; + long now; + synchronized (this) { + record = last; + c = count; + g = generation; + t = thrown; + msl = minMillis; + msh = maxMillis; + now = System.currentTimeMillis(); + if (c == 0L) { + msh = now; + } + + if (reset) { //BUG ID 6351685 + reset(msh); + } + } + + final String head; + final String msg; + final String tail; + final Formatter f = this.formatter; + if (f != null) { + synchronized (f) { + head = f.getHead(h); + msg = record != null ? f.format(record) : ""; + tail = f.getTail(h); + } + } else { + head = ""; + msg = record != null ? formatMessage(record) : ""; + tail = ""; + } + + Locale l = null; + if (record != null) { + ResourceBundle rb = record.getResourceBundle(); + l = rb == null ? null : rb.getLocale(); + } + + final MessageFormat mf; + if (l == null) { //BUG ID 8039165 + mf = new MessageFormat(fmt); + } else { + mf = new MessageFormat(fmt, l); + } + + /** + * These arguments are described in the getTail documentation. + */ + return mf.format(new Object[]{finish(head), finish(msg), finish(tail), + c, (c - 1L), t, (c - t), msl, msh, (msh - msl), INIT_TIME, now, + (now - INIT_TIME), g}); + } + + /** + * Applied to the head, format, and tail returned by the target formatter. + * This implementation trims all input strings. + * + * @param s the string to transform. + * @return the transformed string. + * @throws NullPointerException if the given string is null. + */ + protected String finish(String s) { + return s.trim(); + } + + /** + * Peek at the current log record. + * + * @return null or the current log record. + */ + private synchronized LogRecord peek() { + return this.last; + } + + /** + * Updates the summary statistics and stores given LogRecord if the expected + * record matches the current record. + * + * @param e the expected record. + * @param u the update record. + * @return true if the update was performed. + * @throws NullPointerException if the update record is null. + */ + private synchronized boolean acceptAndUpdate(LogRecord e, LogRecord u) { + if (accept(e, u)) { + this.last = u; + return true; + } else { + return false; + } + } + + /** + * Gets the message format string from the LogManager or creates the default + * message format string. + * + * @param p the class name prefix. + * @return the format string. + * @throws NullPointerException if the given argument is null. + */ + private String initFormat(final String p) { + String v = fromLogManager(p.concat(".format")); + if (v == null || v.length() == 0) { + v = "{0}{1}{2}{4,choice,-1#|0#|0<... {4,number,integer} more}\n"; + } + return v; + } + + /** + * Gets and creates the formatter from the LogManager or creates the default + * formatter. + * + * @param p the class name prefix. + * @return the formatter. + * @throws NullPointerException if the given argument is null. + * @throws UndeclaredThrowableException if the formatter can not be created. + */ + private Formatter initFormatter(final String p) { + Formatter f; + String v = fromLogManager(p.concat(".formatter")); + if (v != null && v.length() != 0) { + if (!"null".equalsIgnoreCase(v)) { + try { + f = LogManagerProperties.newFormatter(v); + } catch (final RuntimeException re) { + throw re; + } catch (final Exception e) { + throw new UndeclaredThrowableException(e); + } + } else { + f = null; + } + } else { + //Don't force the byte code verifier to load the formatter. + f = Formatter.class.cast(new CompactFormatter()); + } + return f; + } + + /** + * Gets and creates the comparator from the LogManager or returns the + * default comparator. + * + * @param p the class name prefix. + * @return the comparator or null. + * @throws IllegalArgumentException if it was specified that the comparator + * should be reversed but no initial comparator was specified. + * @throws NullPointerException if the given argument is null. + * @throws UndeclaredThrowableException if the comparator can not be + * created. + */ + @SuppressWarnings("unchecked") + private Comparator initComparator(final String p) { + Comparator c; + final String name = fromLogManager(p.concat(".comparator")); + final String reverse = fromLogManager(p.concat(".comparator.reverse")); + try { + if (name != null && name.length() != 0) { + if (!"null".equalsIgnoreCase(name)) { + c = LogManagerProperties.newComparator(name); + if (Boolean.parseBoolean(reverse)) { + assert c != null; + c = LogManagerProperties.reverseOrder(c); + } + } else { + if (reverse != null) { + throw new IllegalArgumentException( + "No comparator to reverse."); + } else { + c = null; //No ordering. + } + } + } else { + if (reverse != null) { + throw new IllegalArgumentException( + "No comparator to reverse."); + } else { + //Don't force the byte code verifier to load the comparator. + c = Comparator.class.cast(SeverityComparator.getInstance()); + } + } + } catch (final RuntimeException re) { + throw re; //Avoid catch all. + } catch (final Exception e) { + throw new UndeclaredThrowableException(e); + } + return c; + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/CompactFormatter.java b/app/src/main/java/com/sun/mail/util/logging/CompactFormatter.java new file mode 100644 index 0000000000..7a1a2fb961 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/CompactFormatter.java @@ -0,0 +1,865 @@ +/* + * Copyright (c) 2013, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2019 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.util.*; +import java.util.logging.LogRecord; + +/** + * A plain text formatter that can produce fixed width output. By default this + * formatter will produce output no greater than 160 characters wide plus the + * separator and newline characters. Only specified fields support an + * {@linkplain #toAlternate(java.lang.String) alternate} fixed width format. + *

+ * By default each CompactFormatter is initialized using the + * following LogManager configuration properties where + * <formatter-name> refers to the fully qualified class name + * or the fully qualified derived class name of the formatter. If properties are + * not defined, or contain invalid values, then the specified default values are + * used. + *

    + *
  • <formatter-name>.format - the {@linkplain java.util.Formatter + * format} string used to transform the output. The format string can be + * used to fix the output size. (defaults to %7$#.160s%n)
  • + *
+ * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class CompactFormatter extends java.util.logging.Formatter { + + /** + * Load any declared classes to workaround GLASSFISH-21258. + */ + static { + loadDeclaredClasses(); + } + + /** + * Used to load declared classes encase class loader doesn't allow loading + * during JVM termination. This method is used with unit testing. + * + * @return an array of classes never null. + */ + private static Class[] loadDeclaredClasses() { + return new Class[]{Alternate.class}; + } + + /** + * Holds the java.util.Formatter pattern. + */ + private final String fmt; + + /** + * Creates an instance with a default format pattern. + */ + public CompactFormatter() { + String p = getClass().getName(); + this.fmt = initFormat(p); + } + + /** + * Creates an instance with the given format pattern. + * + * @param format the {@linkplain java.util.Formatter pattern} or null to use + * the LogManager default. The arguments are described in the + * {@linkplain #format(java.util.logging.LogRecord) format} method. + */ + public CompactFormatter(final String format) { + String p = getClass().getName(); + this.fmt = format == null ? initFormat(p) : format; + } + + /** + * Format the given log record and returns the formatted string. The + * {@linkplain java.util.Formatter#format(java.lang.String, java.lang.Object...) + * java.util} argument indexes are assigned to the following properties: + * + *
    + *
  1. {@code format} - the {@linkplain java.util.Formatter + * java.util.Formatter} format string specified in the + * <formatter-name>.format property or the format that was given when + * this formatter was created.
  2. + *
  3. {@code date} - if the log record supports nanoseconds then a + * ZonedDateTime object representing the event time of the log record in the + * system time zone. Otherwise, a {@linkplain Date} object representing + * {@linkplain LogRecord#getMillis event time} of the log record.
  4. + *
  5. {@code source} - a string representing the caller, if available; + * otherwise, the logger's name.
  6. + *
  7. {@code logger} - the logger's + * {@linkplain Class#getSimpleName() simple} + * {@linkplain LogRecord#getLoggerName() name}.
  8. + *
  9. {@code level} - the + * {@linkplain java.util.logging.Level#getLocalizedName log level}.
  10. + *
  11. {@code message} - the formatted log message returned from the + * {@linkplain #formatMessage(LogRecord)} method.
  12. + *
  13. {@code thrown} - a string representing the + * {@linkplain LogRecord#getThrown throwable} associated with the log record + * and a relevant stack trace element if available; otherwise, an empty + * string is used.
  14. + *
  15. {@code message|thrown} The message and the thrown properties joined + * as one parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.
  16. + *
  17. {@code thrown|message} The thrown and message properties joined as + * one parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.
  18. + *
  19. {@code sequence} the + * {@linkplain LogRecord#getSequenceNumber() sequence number} if the given + * log record.
  20. + *
  21. {@code thread id} the {@linkplain LogRecord#getThreadID() thread id} + * of the given log record. By default this is formatted as a {@code long} + * by an unsigned conversion.
  22. + *
  23. {@code error} the throwable + * {@linkplain Class#getSimpleName() simple class name} and + * {@linkplain #formatError(LogRecord) error message} without any stack + * trace.
  24. + *
  25. {@code message|error} The message and error properties joined as one + * parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.
  26. + *
  27. {@code error|message} The error and message properties joined as one + * parameter. This parameter supports + * {@linkplain #toAlternate(java.lang.String) alternate} form.
  28. + *
  29. {@code backtrace} only the + * {@linkplain #formatBackTrace(LogRecord) stack trace} of the given + * throwable.
  30. + *
  31. {@code bundlename} the resource bundle + * {@linkplain LogRecord#getResourceBundleName() name} of the given log + * record.
  32. + *
  33. {@code key} the {@linkplain LogRecord#getMessage() raw message} + * before localization or formatting.
  34. + *
+ * + *

+ * Some example formats:
+ *

    + *
  • {@code com.sun.mail.util.logging.CompactFormatter.format=%7$#.160s%n} + *

    + * This prints only 160 characters of the message|thrown ({@code 7$}) using + * the {@linkplain #toAlternate(java.lang.String) alternate} form. The + * separator is not included as part of the total width. + *

    +     * Encoding failed.|NullPointerException: null String.getBytes(:913)
    +     * 
    + * + *
  • {@code com.sun.mail.util.logging.CompactFormatter.format=%7$#.20s%n} + *

    + * This prints only 20 characters of the message|thrown ({@code 7$}) using + * the {@linkplain #toAlternate(java.lang.String) alternate} form. This will + * perform a weighted truncation of both the message and thrown properties + * of the log record. The separator is not included as part of the total + * width. + *

    +     * Encoding|NullPointerE
    +     * 
    + * + *
  • {@code com.sun.mail.util.logging.CompactFormatter.format=%1$tc %2$s%n%4$s: %5$s%6$s%n} + *

    + * This prints the timestamp ({@code 1$}) and the source ({@code 2$}) on the + * first line. The second line is the log level ({@code 4$}), log message + * ({@code 5$}), and the throwable with a relevant stack trace element + * ({@code 6$}) if one is available. + *

    +     * Fri Nov 20 07:29:24 CST 2009 MyClass fatal
    +     * SEVERE: Encoding failed.NullPointerException: null String.getBytes(:913)
    +     * 
    + * + *
  • {@code com.sun.mail.util.logging.CompactFormatter.format=%4$s: %12$#.160s%n} + *

    + * This prints the log level ({@code 4$}) and only 160 characters of the + * message|error ({@code 12$}) using the alternate form. + *

    +     * SEVERE: Unable to send notification.|SocketException: Permission denied: connect
    +     * 
    + * + *
  • {@code com.sun.mail.util.logging.CompactFormatter.format=[%9$d][%1$tT][%10$d][%2$s] %5$s%n%6$s%n} + *

    + * This prints the sequence ({@code 9$}), event time ({@code 1$}) as 24 hour + * time, thread id ({@code 10$}), source ({@code 2$}), log message + * ({@code 5$}), and the throwable with back trace ({@code 6$}). + *

    +     * [125][14:11:42][38][MyClass fatal] Unable to send notification.
    +     * SocketException: Permission denied: connect SMTPTransport.openServer(:1949)
    +     * 
    + * + *
+ * + * @param record the log record to format. + * @return the formatted record. + * @throws NullPointerException if the given record is null. + */ + @Override + public String format(final LogRecord record) { + //LogRecord is mutable so define local vars. + ResourceBundle rb = record.getResourceBundle(); + Locale l = rb == null ? null : rb.getLocale(); + + String msg = formatMessage(record); + String thrown = formatThrown(record); + String err = formatError(record); + Object[] params = { + formatZonedDateTime(record), + formatSource(record), + formatLoggerName(record), + formatLevel(record), + msg, + thrown, + new Alternate(msg, thrown), + new Alternate(thrown, msg), + record.getSequenceNumber(), + formatThreadID(record), + err, + new Alternate(msg, err), + new Alternate(err, msg), + formatBackTrace(record), + record.getResourceBundleName(), + record.getMessage()}; + + if (l == null) { //BUG ID 6282094 + return String.format(fmt, params); + } else { + return String.format(l, fmt, params); + } + } + + /** + * Formats message for the log record. This method removes any fully + * qualified throwable class names from the message. + * + * @param record the log record. + * @return the formatted message string. + */ + @Override + public String formatMessage(final LogRecord record) { + String msg = super.formatMessage(record); + msg = replaceClassName(msg, record.getThrown()); + msg = replaceClassName(msg, record.getParameters()); + return msg; + } + + /** + * Formats the message from the thrown property of the log record. This + * method replaces fully qualified throwable class names from the message + * cause chain with simple class names. + * + * @param t the throwable to format or null. + * @return the empty string if null was given or the formatted message + * string from the throwable which may be null. + */ + public String formatMessage(final Throwable t) { + String r; + if (t != null) { + final Throwable apply = apply(t); + final String m = apply.getLocalizedMessage(); + final String s = apply.toString(); + final String sn = simpleClassName(apply.getClass()); + if (!isNullOrSpaces(m)) { + if (s.contains(m)) { + if (s.startsWith(apply.getClass().getName()) + || s.startsWith(sn)) { + r = replaceClassName(m, t); + } else { + r = replaceClassName(simpleClassName(s), t); + } + } else { + r = replaceClassName(simpleClassName(s) + ": " + m, t); + } + } else { + r = replaceClassName(simpleClassName(s), t); + } + + if (!r.contains(sn)) { + r = sn + ": " + r; + } + } else { + r = ""; + } + return r; + } + + /** + * Formats the level property of the given log record. + * + * @param record the record. + * @return the formatted logger name. + * @throws NullPointerException if the given record is null. + */ + public String formatLevel(final LogRecord record) { + return record.getLevel().getLocalizedName(); + } + + /** + * Formats the source from the given log record. + * + * @param record the record. + * @return the formatted source of the log record. + * @throws NullPointerException if the given record is null. + */ + public String formatSource(final LogRecord record) { + String source = record.getSourceClassName(); + if (source != null) { + if (record.getSourceMethodName() != null) { + source = simpleClassName(source) + " " + + record.getSourceMethodName(); + } else { + source = simpleClassName(source); + } + } else { + source = simpleClassName(record.getLoggerName()); + } + return source; + } + + /** + * Formats the logger name property of the given log record. + * + * @param record the record. + * @return the formatted logger name. + * @throws NullPointerException if the given record is null. + */ + public String formatLoggerName(final LogRecord record) { + return simpleClassName(record.getLoggerName()); + } + + /** + * Formats the thread id property of the given log record. By default this + * is formatted as a {@code long} by an unsigned conversion. + * + * @param record the record. + * @return the formatted thread id as a number. + * @throws NullPointerException if the given record is null. + * @since JavaMail 1.5.4 + */ + public Number formatThreadID(final LogRecord record) { + /** + * Thread.getID is defined as long and LogRecord.getThreadID is defined + * as int. Convert to unsigned as a means to better map the two types of + * thread identifiers. + */ + return (((long) record.getThreadID()) & 0xffffffffL); + } + + /** + * Formats the thrown property of a LogRecord. The returned string will + * contain a throwable message with a back trace. + * + * @param record the record. + * @return empty string if nothing was thrown or formatted string. + * @throws NullPointerException if the given record is null. + * @see #apply(java.lang.Throwable) + * @see #formatBackTrace(java.util.logging.LogRecord) + */ + public String formatThrown(final LogRecord record) { + String msg; + final Throwable t = record.getThrown(); + if (t != null) { + String site = formatBackTrace(record); + msg = formatMessage(t) + (isNullOrSpaces(site) ? "" : ' ' + site); + } else { + msg = ""; + } + return msg; + } + + /** + * Formats the thrown property of a LogRecord as an error message. The + * returned string will not contain a back trace. + * + * @param record the record. + * @return empty string if nothing was thrown or formatted string. + * @throws NullPointerException if the given record is null. + * @see Throwable#toString() + * @see #apply(java.lang.Throwable) + * @see #formatMessage(java.lang.Throwable) + * @since JavaMail 1.5.4 + */ + public String formatError(final LogRecord record) { + return formatMessage(record.getThrown()); + } + + /** + * Formats the back trace for the given log record. + * + * @param record the log record to format. + * @return the formatted back trace. + * @throws NullPointerException if the given record is null. + * @see #apply(java.lang.Throwable) + * @see #formatThrown(java.util.logging.LogRecord) + * @see #ignore(java.lang.StackTraceElement) + */ + public String formatBackTrace(final LogRecord record) { + String site = ""; + final Throwable t = record.getThrown(); + if (t != null) { + final Throwable root = apply(t); + StackTraceElement[] trace = root.getStackTrace(); + site = findAndFormat(trace); + if (isNullOrSpaces(site)) { + int limit = 0; + for (Throwable c = t; c != null; c = c.getCause()) { + StackTraceElement[] ste = c.getStackTrace(); + site = findAndFormat(ste); + if (!isNullOrSpaces(site)) { + break; + } else { + if (trace.length == 0) { + trace = ste; + } + } + + //Deal with excessive cause chains + //and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + + //Punt. + if (isNullOrSpaces(site) && trace.length != 0) { + site = formatStackTraceElement(trace[0]); + } + } + } + return site; + } + + /** + * Finds and formats the first stack frame of interest. + * + * @param trace the fill stack to examine. + * @return a String that best describes the call site. + * @throws NullPointerException if stack trace element array is null. + */ + private String findAndFormat(final StackTraceElement[] trace) { + String site = ""; + for (StackTraceElement s : trace) { + if (!ignore(s)) { + site = formatStackTraceElement(s); + break; + } + } + + //Check if all code was compiled with no debugging info. + if (isNullOrSpaces(site)) { + for (StackTraceElement s : trace) { + if (!defaultIgnore(s)) { + site = formatStackTraceElement(s); + break; + } + } + } + return site; + } + + /** + * Formats a stack trace element into a simple call site. + * + * @param s the stack trace element to format. + * @return the formatted stack trace element. + * @throws NullPointerException if stack trace element is null. + * @see #formatThrown(java.util.logging.LogRecord) + */ + private String formatStackTraceElement(final StackTraceElement s) { + String v = simpleClassName(s.getClassName()); + String result; + if (v != null) { + result = s.toString().replace(s.getClassName(), v); + } else { + result = s.toString(); + } + + //If the class name contains the simple file name then remove file name. + v = simpleFileName(s.getFileName()); + if (v != null && result.startsWith(v)) { + result = result.replace(s.getFileName(), ""); + } + return result; + } + + /** + * Chooses a single throwable from the cause chain that will be formatted. + * This implementation chooses the throwable that best describes the chain. + * Subclasses can override this method to choose an alternate throwable for + * formatting. + * + * @param t the throwable from the log record. + * @return the chosen throwable or null only if the given argument is null. + * @see #formatThrown(java.util.logging.LogRecord) + */ + protected Throwable apply(final Throwable t) { + return SeverityComparator.getInstance().apply(t); + } + + /** + * Determines if a stack frame should be ignored as the cause of an error. + * + * @param s the stack trace element. + * @return true if this frame should be ignored. + * @see #formatThrown(java.util.logging.LogRecord) + */ + protected boolean ignore(final StackTraceElement s) { + return isUnknown(s) || defaultIgnore(s); + } + + /** + * Defines the alternate format. This implementation removes all control + * characters from the given string. + * + * @param s any string or null. + * @return null if the argument was null otherwise, an alternate string. + */ + protected String toAlternate(final String s) { + return s != null ? s.replaceAll("[\\x00-\\x1F\\x7F]+", "") : null; + } + + /** + * Gets the zoned date time from the given log record. + * + * @param record the current log record. + * @return a zoned date time or a legacy date object. + * @throws NullPointerException if the given record is null. + * @since JavaMail 1.5.6 + */ + private Comparable formatZonedDateTime(final LogRecord record) { + Comparable zdt = LogManagerProperties.getZonedDateTime(record); + if (zdt == null) { + zdt = new java.util.Date(record.getMillis()); + } + return zdt; + } + + /** + * Determines if a stack frame should be ignored as the cause of an error. + * This does not check for unknown line numbers because code can be compiled + * without debugging info. + * + * @param s the stack trace element. + * @return true if this frame should be ignored. + */ + private boolean defaultIgnore(final StackTraceElement s) { + return isSynthetic(s) || isStaticUtility(s) || isReflection(s); + } + + /** + * Determines if a stack frame is for a static utility class. + * + * @param s the stack trace element. + * @return true if this frame should be ignored. + */ + private boolean isStaticUtility(final StackTraceElement s) { + try { + return LogManagerProperties.isStaticUtilityClass(s.getClassName()); + } catch (RuntimeException ignore) { + } catch (Exception | LinkageError ignore) { + } + final String cn = s.getClassName(); + return (cn.endsWith("s") && !cn.endsWith("es")) + || cn.contains("Util") || cn.endsWith("Throwables"); + } + + /** + * Determines if a stack trace element is for a synthetic method. + * + * @param s the stack trace element. + * @return true if synthetic. + * @throws NullPointerException if stack trace element is null. + */ + private boolean isSynthetic(final StackTraceElement s) { + return s.getMethodName().indexOf('$') > -1; + } + + /** + * Determines if a stack trace element has an unknown line number or a + * native line number. + * + * @param s the stack trace element. + * @return true if the line number is unknown. + * @throws NullPointerException if stack trace element is null. + */ + private boolean isUnknown(final StackTraceElement s) { + return s.getLineNumber() < 0; + } + + /** + * Determines if a stack trace element represents a reflection frame. + * + * @param s the stack trace element. + * @return true if the line number is unknown. + * @throws NullPointerException if stack trace element is null. + */ + private boolean isReflection(final StackTraceElement s) { + try { + return LogManagerProperties.isReflectionClass(s.getClassName()); + } catch (RuntimeException ignore) { + } catch (Exception | LinkageError ignore) { + } + return s.getClassName().startsWith("java.lang.reflect.") + || s.getClassName().startsWith("sun.reflect."); + } + + /** + * Creates the format pattern for this formatter. + * + * @param p the class name prefix. + * @return the java.util.Formatter format string. + * @throws NullPointerException if the given class name is null. + */ + private String initFormat(final String p) { + String v = LogManagerProperties.fromLogManager(p.concat(".format")); + if (isNullOrSpaces(v)) { + v = "%7$#.160s%n"; //160 chars split between message and thrown. + } + return v; + } + + /** + * Searches the given message for all instances fully qualified class name + * with simple class name based off of the types contained in the given + * parameter array. + * + * @param msg the message. + * @param t the throwable cause chain to search or null. + * @return the modified message string. + */ + private static String replaceClassName(String msg, Throwable t) { + if (!isNullOrSpaces(msg)) { + int limit = 0; + for (Throwable c = t; c != null; c = c.getCause()) { + final Class k = c.getClass(); + msg = msg.replace(k.getName(), simpleClassName(k)); + + //Deal with excessive cause chains and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + } + return msg; + } + + /** + * Searches the given message for all instances fully qualified class name + * with simple class name based off of the types contained in the given + * parameter array. + * + * @param msg the message or null. + * @param p the parameter array or null. + * @return the modified message string. + */ + private static String replaceClassName(String msg, Object[] p) { + if (!isNullOrSpaces(msg) && p != null) { + for (Object o : p) { + if (o != null) { + final Class k = o.getClass(); + msg = msg.replace(k.getName(), simpleClassName(k)); + } + } + } + return msg; + } + + /** + * Gets the simple class name from the given class. This is a workaround for + * BUG ID JDK-8057919. + * + * @param k the class object. + * @return the simple class name or null. + * @since JavaMail 1.5.3 + */ + private static String simpleClassName(final Class k) { + try { + return k.getSimpleName(); + } catch (final InternalError JDK8057919) { + } + return simpleClassName(k.getName()); + } + + /** + * Converts a fully qualified class name to a simple class name. If the + * leading part of the given string is not a legal class name then the given + * string is returned. + * + * @param name the fully qualified class name prefix or null. + * @return the simple class name or given input. + */ + private static String simpleClassName(String name) { + if (name != null) { + int cursor = 0; + int sign = -1; + int dot = -1; + for (int c, prev = dot; cursor < name.length(); + cursor += Character.charCount(c)) { + c = name.codePointAt(cursor); + if (!Character.isJavaIdentifierPart(c)) { + if (c == ((int) '.')) { + if ((dot + 1) != cursor && (dot + 1) != sign) { + prev = dot; + dot = cursor; + } else { + return name; + } + } else { + if ((dot + 1) == cursor) { + dot = prev; + } + break; + } + } else { + if (c == ((int) '$')) { + sign = cursor; + } + } + } + + if (dot > -1 && ++dot < cursor && ++sign < cursor) { + name = name.substring(sign > dot ? sign : dot); + } + } + return name; + } + + /** + * Converts a file name with an extension to a file name without an + * extension. + * + * @param name the full file name or null. + * @return the simple file name or null. + */ + private static String simpleFileName(String name) { + if (name != null) { + final int index = name.lastIndexOf('.'); + name = index > -1 ? name.substring(0, index) : name; + } + return name; + } + + /** + * Determines is the given string is null or spaces. + * + * @param s the string or null. + * @return true if null or spaces. + */ + private static boolean isNullOrSpaces(final String s) { + return s == null || s.trim().length() == 0; + } + + /** + * Used to format two arguments as fixed length message. + */ + private class Alternate implements java.util.Formattable { + + /** + * The left side of the output. + */ + private final String left; + /** + * The right side of the output. + */ + private final String right; + + /** + * Creates an alternate output. + * + * @param left the left side or null. + * @param right the right side or null. + */ + Alternate(final String left, final String right) { + this.left = String.valueOf(left); + this.right = String.valueOf(right); + } + + @SuppressWarnings("override") //JDK-6954234 + public void formatTo(java.util.Formatter formatter, int flags, + int width, int precision) { + + String l = left; + String r = right; + if ((flags & java.util.FormattableFlags.UPPERCASE) + == java.util.FormattableFlags.UPPERCASE) { + l = l.toUpperCase(formatter.locale()); + r = r.toUpperCase(formatter.locale()); + } + + if ((flags & java.util.FormattableFlags.ALTERNATE) + == java.util.FormattableFlags.ALTERNATE) { + l = toAlternate(l); + r = toAlternate(r); + } + + if (precision <= 0) { + precision = Integer.MAX_VALUE; + } + + int fence = Math.min(l.length(), precision); + if (fence > (precision >> 1)) { + fence = Math.max(fence - r.length(), fence >> 1); + } + + if (fence > 0) { + if (fence > l.length() + && Character.isHighSurrogate(l.charAt(fence - 1))) { + --fence; + } + l = l.substring(0, fence); + } + r = r.substring(0, Math.min(precision - fence, r.length())); + + if (width > 0) { + final int half = width >> 1; + if (l.length() < half) { + l = pad(flags, l, half); + } + + if (r.length() < half) { + r = pad(flags, r, half); + } + } + + Object[] empty = Collections.emptySet().toArray(); + formatter.format(l, empty); + if (l.length() != 0 && r.length() != 0) { + formatter.format("|", empty); + } + formatter.format(r, empty); + } + + /** + * Pad the given input string. + * + * @param flags the formatter flags. + * @param s the string to pad. + * @param length the final string length. + * @return the padded string. + */ + private String pad(int flags, String s, int length) { + final int padding = length - s.length(); + final StringBuilder b = new StringBuilder(length); + if ((flags & java.util.FormattableFlags.LEFT_JUSTIFY) + == java.util.FormattableFlags.LEFT_JUSTIFY) { + for (int i = 0; i < padding; ++i) { + b.append('\u0020'); + } + b.append(s); + } else { + b.append(s); + for (int i = 0; i < padding; ++i) { + b.append('\u0020'); + } + } + return b.toString(); + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/DurationFilter.java b/app/src/main/java/com/sun/mail/util/logging/DurationFilter.java new file mode 100644 index 0000000000..b85101468c --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/DurationFilter.java @@ -0,0 +1,444 @@ +/* + * Copyright (c) 2015, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2015, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import static com.sun.mail.util.logging.LogManagerProperties.fromLogManager; +import java.util.logging.*; + +/** + * A filter used to limit log records based on a maximum generation rate. + * + * The duration specified is used to compute the record rate and the amount of + * time the filter will reject records once the rate has been exceeded. Once the + * rate is exceeded records are not allowed until the duration has elapsed. + * + *

+ * By default each {@code DurationFilter} is initialized using the following + * LogManager configuration properties where {@code } refers to the + * fully qualified class name of the handler. If properties are not defined, or + * contain invalid values, then the specified default values are used. + * + *

    + *
  • {@literal }.records the max number of records per duration. + * A numeric long integer or a multiplication expression can be used as the + * value. (defaults to {@code 1000}) + * + *
  • {@literal }.duration the number of milliseconds to suppress + * log records from being published. This is also used as duration to determine + * the log record rate. A numeric long integer or a multiplication expression + * can be used as the value. If the {@code java.time} package is available then + * an ISO-8601 duration format of {@code PnDTnHnMn.nS} can be used as the value. + * The suffixes of "D", "H", "M" and "S" are for days, hours, minutes and + * seconds. The suffixes must occur in order. The seconds can be specified with + * a fractional component to declare milliseconds. (defaults to {@code PT15M}) + *
+ * + *

+ * For example, the settings to limit {@code MailHandler} with a default + * capacity to only send a maximum of two email messages every six minutes would + * be as follows: + *

+ * {@code
+ *  com.sun.mail.util.logging.MailHandler.filter = com.sun.mail.util.logging.DurationFilter
+ *  com.sun.mail.util.logging.MailHandler.capacity = 1000
+ *  com.sun.mail.util.logging.DurationFilter.records = 2L * 1000L
+ *  com.sun.mail.util.logging.DurationFilter.duration = PT6M
+ * }
+ * 
+ * + * + * @author Jason Mehrens + * @since JavaMail 1.5.5 + */ +public class DurationFilter implements Filter { + + /** + * The number of expected records per duration. + */ + private final long records; + /** + * The duration in milliseconds used to determine the rate. The duration is + * also used as the amount of time that the filter will not allow records + * when saturated. + */ + private final long duration; + /** + * The number of records seen for the current duration. This value negative + * if saturated. Zero is considered saturated but is reserved for recording + * the first duration. + */ + private long count; + /** + * The most recent record time seen for the current duration. + */ + private long peak; + /** + * The start time for the current duration. + */ + private long start; + + /** + * Creates the filter using the default properties. + */ + public DurationFilter() { + this.records = checkRecords(initLong(".records")); + this.duration = checkDuration(initLong(".duration")); + } + + /** + * Creates the filter using the given properties. Default values are used if + * any of the given values are outside the allowed range. + * + * @param records the number of records per duration. + * @param duration the number of milliseconds to suppress log records from + * being published. + */ + public DurationFilter(final long records, final long duration) { + this.records = checkRecords(records); + this.duration = checkDuration(duration); + } + + /** + * Determines if this filter is equal to another filter. + * + * @param obj the given object. + * @return true if equal otherwise false. + */ + @Override + public boolean equals(final Object obj) { + if (this == obj) { //Avoid locks and deal with rapid state changes. + return true; + } + + if (obj == null || getClass() != obj.getClass()) { + return false; + } + + final DurationFilter other = (DurationFilter) obj; + if (this.records != other.records) { + return false; + } + + if (this.duration != other.duration) { + return false; + } + + final long c; + final long p; + final long s; + synchronized (this) { + c = this.count; + p = this.peak; + s = this.start; + } + + synchronized (other) { + if (c != other.count || p != other.peak || s != other.start) { + return false; + } + } + return true; + } + + /** + * Determines if this filter is able to accept the maximum number of log + * records for this instant in time. The result is a best-effort estimate + * and should be considered out of date as soon as it is produced. This + * method is designed for use in monitoring the state of this filter. + * + * @return true if the filter is idle; false otherwise. + */ + public boolean isIdle() { + return test(0L, System.currentTimeMillis()); + } + + /** + * Returns a hash code value for this filter. + * + * @return hash code for this filter. + */ + @Override + public int hashCode() { + int hash = 3; + hash = 89 * hash + (int) (this.records ^ (this.records >>> 32)); + hash = 89 * hash + (int) (this.duration ^ (this.duration >>> 32)); + return hash; + } + + /** + * Check if the given log record should be published. This method will + * modify the internal state of this filter. + * + * @param record the log record to check. + * @return true if allowed; false otherwise. + * @throws NullPointerException if given record is null. + */ + @SuppressWarnings("override") //JDK-6954234 + public boolean isLoggable(final LogRecord record) { + return accept(record.getMillis()); + } + + /** + * Determines if this filter will accept log records for this instant in + * time. The result is a best-effort estimate and should be considered out + * of date as soon as it is produced. This method is designed for use in + * monitoring the state of this filter. + * + * @return true if the filter is not saturated; false otherwise. + */ + public boolean isLoggable() { + return test(records, System.currentTimeMillis()); + } + + /** + * Returns a string representation of this filter. The result is a + * best-effort estimate and should be considered out of date as soon as it + * is produced. + * + * @return a string representation of this filter. + */ + @Override + public String toString() { + boolean idle; + boolean loggable; + synchronized (this) { + final long millis = System.currentTimeMillis(); + idle = test(0L, millis); + loggable = test(records, millis); + } + + return getClass().getName() + "{records=" + records + + ", duration=" + duration + + ", idle=" + idle + + ", loggable=" + loggable + '}'; + } + + /** + * Creates a copy of this filter that retains the filter settings but does + * not include the current filter state. The newly create clone acts as if + * it has never seen any records. + * + * @return a copy of this filter. + * @throws CloneNotSupportedException if this filter is not allowed to be + * cloned. + */ + @Override + protected DurationFilter clone() throws CloneNotSupportedException { + final DurationFilter clone = (DurationFilter) super.clone(); + clone.count = 0L; //Reset the filter state. + clone.peak = 0L; + clone.start = 0L; + return clone; + } + + /** + * Checks if this filter is not saturated or bellow a maximum rate. + * + * @param limit the number of records allowed to be under the rate. + * @param millis the current time in milliseconds. + * @return true if not saturated or bellow the rate. + */ + private boolean test(final long limit, final long millis) { + assert limit >= 0L : limit; + final long c; + final long s; + synchronized (this) { + c = count; + s = start; + } + + if (c > 0L) { //If not saturated. + if ((millis - s) >= duration || c < limit) { + return true; + } + } else { //Subtraction is used to deal with numeric overflow. + if ((millis - s) >= 0L || c == 0L) { + return true; + } + } + return false; + } + + /** + * Determines if the record is loggable by time. + * + * @param millis the log record milliseconds. + * @return true if accepted false otherwise. + */ + private synchronized boolean accept(final long millis) { + //Subtraction is used to deal with numeric overflow of millis. + boolean allow; + if (count > 0L) { //If not saturated. + if ((millis - peak) > 0L) { + peak = millis; //Record the new peak. + } + + //Under the rate if the count has not been reached. + if (count != records) { + ++count; + allow = true; + } else { + if ((peak - start) >= duration) { + count = 1L; //Start a new duration. + start = peak; + allow = true; + } else { + count = -1L; //Saturate for the duration. + start = peak + duration; + allow = false; + } + } + } else { + //If the saturation period has expired or this is the first record + //then start a new duration and allow records. + if ((millis - start) >= 0L || count == 0L) { + count = 1L; + start = millis; + peak = millis; + allow = true; + } else { + allow = false; //Remain in a saturated state. + } + } + return allow; + } + + /** + * Reads a long value or multiplication expression from the LogManager. If + * the value can not be parsed or is not defined then Long.MIN_VALUE is + * returned. + * + * @param suffix a dot character followed by the key name. + * @return a long value or Long.MIN_VALUE if unable to parse or undefined. + * @throws NullPointerException if suffix is null. + */ + private long initLong(final String suffix) { + long result = 0L; + final String p = getClass().getName(); + String value = fromLogManager(p.concat(suffix)); + if (value != null && value.length() != 0) { + value = value.trim(); + if (isTimeEntry(suffix, value)) { + try { + result = LogManagerProperties.parseDurationToMillis(value); + } catch (final RuntimeException ignore) { + } catch (final Exception ignore) { + } catch (final LinkageError ignore) { + } + } + + if (result == 0L) { //Zero is invalid. + try { + result = 1L; + for (String s : tokenizeLongs(value)) { + if (s.endsWith("L") || s.endsWith("l")) { + s = s.substring(0, s.length() - 1); + } + result = multiplyExact(result, Long.parseLong(s)); + } + } catch (final RuntimeException ignore) { + result = Long.MIN_VALUE; + } + } + } else { + result = Long.MIN_VALUE; + } + return result; + } + + /** + * Determines if the given suffix can be a time unit and the value is + * encoded as an ISO ISO-8601 duration format. + * + * @param suffix the suffix property. + * @param value the value of the property. + * @return true if the entry is a time entry. + * @throws IndexOutOfBoundsException if value is empty. + * @throws NullPointerException if either argument is null. + */ + private boolean isTimeEntry(final String suffix, final String value) { + return (value.charAt(0) == 'P' || value.charAt(0) == 'p') + && suffix.equals(".duration"); + } + + /** + * Parse any long value or multiplication expressions into tokens. + * + * @param value the expression or value. + * @return an array of long tokens, never empty. + * @throws NullPointerException if the given value is null. + * @throws NumberFormatException if the expression is invalid. + */ + private static String[] tokenizeLongs(final String value) { + String[] e; + final int i = value.indexOf('*'); + if (i > -1 && (e = value.split("\\s*\\*\\s*")).length != 0) { + if (i == 0 || value.charAt(value.length() - 1) == '*') { + throw new NumberFormatException(value); + } + + if (e.length == 1) { + throw new NumberFormatException(e[0]); + } + } else { + e = new String[]{value}; + } + return e; + } + + /** + * Multiply and check for overflow. This can be replaced with + * {@code java.lang.Math.multiplyExact} when Jakarta Mail requires JDK 8. + * + * @param x the first value. + * @param y the second value. + * @return x times y. + * @throws ArithmeticException if overflow is detected. + */ + private static long multiplyExact(final long x, final long y) { + long r = x * y; + if (((Math.abs(x) | Math.abs(y)) >>> 31L != 0L)) { + if (((y != 0L) && (r / y != x)) + || (x == Long.MIN_VALUE && y == -1L)) { + throw new ArithmeticException(); + } + } + return r; + } + + /** + * Converts record count to a valid record count. If the value is out of + * bounds then the default record count is used. + * + * @param records the record count. + * @return a valid number of record count. + */ + private static long checkRecords(final long records) { + return records > 0L ? records : 1000L; + } + + /** + * Converts the duration to a valid duration. If the value is out of bounds + * then the default duration is used. + * + * @param duration the duration to check. + * @return a valid duration. + */ + private static long checkDuration(final long duration) { + return duration > 0L ? duration : 15L * 60L * 1000L; + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/LogManagerProperties.java b/app/src/main/java/com/sun/mail/util/logging/LogManagerProperties.java new file mode 100644 index 0000000000..e2ec6c575e --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/LogManagerProperties.java @@ -0,0 +1,1090 @@ +/* + * Copyright (c) 2009, 2019 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2019 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.lang.reflect.UndeclaredThrowableException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.*; +import java.util.logging.*; +import java.util.logging.Formatter; + +/** + * An adapter class to allow the Mail API to access the LogManager properties. + * The LogManager properties are treated as the root of all properties. First, + * the parent properties are searched. If no value is found, then, the + * LogManager is searched with prefix value. If not found, then, just the key + * itself is searched in the LogManager. If a value is found in the LogManager + * it is then copied to this properties object with no key prefix. If no value + * is found in the LogManager or the parent properties, then this properties + * object is searched only by passing the key value. + * + *

+ * This class also emulates the LogManager functions for creating new objects + * from string class names. This is to support initial setup of objects such as + * log filters, formatters, error managers, etc. + * + *

+ * This class should never be exposed outside of this package. Keep this class + * package private (default access). + * + * @author Jason Mehrens + * @since JavaMail 1.4.3 + */ +final class LogManagerProperties extends Properties { + + /** + * Generated serial id. + */ + private static final long serialVersionUID = -2239983349056806252L; + /** + * Holds the method used to get the LogRecord instant if running on JDK 9 or + * later. + */ + private static final Method LR_GET_INSTANT; + + /** + * Holds the method used to get the default time zone if running on JDK 9 or + * later. + */ + private static final Method ZI_SYSTEM_DEFAULT; + + /** + * Holds the method used to convert and instant to a zoned date time if + * running on JDK 9 later. + */ + private static final Method ZDT_OF_INSTANT; + + static { + Method lrgi = null; + Method zisd = null; + Method zdtoi = null; + try { + lrgi = LogRecord.class.getMethod("getInstant"); + assert Comparable.class + .isAssignableFrom(lrgi.getReturnType()) : lrgi; + zisd = findClass("java.time.ZoneId") + .getMethod("systemDefault"); + if (!Modifier.isStatic(zisd.getModifiers())) { + throw new NoSuchMethodException(zisd.toString()); + } + + zdtoi = findClass("java.time.ZonedDateTime") + .getMethod("ofInstant", findClass("java.time.Instant"), + findClass("java.time.ZoneId")); + if (!Modifier.isStatic(zdtoi.getModifiers())) { + throw new NoSuchMethodException(zdtoi.toString()); + } + + if (!Comparable.class.isAssignableFrom(zdtoi.getReturnType())) { + throw new NoSuchMethodException(zdtoi.toString()); + } + } catch (final RuntimeException ignore) { + } catch (final Exception ignore) { //No need for specific catch. + } catch (final LinkageError ignore) { + } finally { + if (lrgi == null || zisd == null || zdtoi == null) { + lrgi = null; //If any are null then clear all. + zisd = null; + zdtoi = null; + } + } + + LR_GET_INSTANT = lrgi; + ZI_SYSTEM_DEFAULT = zisd; + ZDT_OF_INSTANT = zdtoi; + } + /** + * Caches the read only reflection class names string array. Declared + * volatile for safe publishing only. The VO_VOLATILE_REFERENCE_TO_ARRAY + * warning is a false positive. + */ + @SuppressWarnings("VolatileArrayField") + private static volatile String[] REFLECT_NAMES; + /** + * Caches the LogManager or Properties so we only read the configuration + * once. + */ + private static final Object LOG_MANAGER = loadLogManager(); + + /** + * Get the LogManager or loads a Properties object to use as the LogManager. + * + * @return the LogManager or a loaded Properties object. + * @since JavaMail 1.5.3 + */ + private static Object loadLogManager() { + Object m; + try { + m = LogManager.getLogManager(); + } catch (final LinkageError restricted) { + m = readConfiguration(); + } catch (final RuntimeException unexpected) { + m = readConfiguration(); + } + return m; + } + + /** + * Create a properties object from the default logging configuration file. + * Since the LogManager is not available in restricted environments, only + * the default configuration is applicable. + * + * @return a properties object loaded with the default configuration. + * @since JavaMail 1.5.3 + */ + private static Properties readConfiguration() { + /** + * Load the properties file so the default settings are available when + * user code creates a logging object. The class loader for the + * restricted LogManager can't access these classes to attach them to a + * logger or handler on startup. Creating logging objects at this point + * is both useless and risky. + */ + final Properties props = new Properties(); + try { + String n = System.getProperty("java.util.logging.config.file"); + if (n != null) { + final File f = new File(n).getCanonicalFile(); + final InputStream in = new FileInputStream(f); + try { + props.load(in); + } finally { + in.close(); + } + } + } catch (final RuntimeException permissionsOrMalformed) { + } catch (final Exception ioe) { + } catch (final LinkageError unexpected) { + } + return props; + } + + /** + * Gets LogManger property for the running JVM. If the LogManager doesn't + * exist then the default LogManger properties are used. + * + * @param name the property name. + * @return the LogManager. + * @throws NullPointerException if the given name is null. + * @since JavaMail 1.5.3 + */ + static String fromLogManager(final String name) { + if (name == null) { + throw new NullPointerException(); + } + + final Object m = LOG_MANAGER; + try { + if (m instanceof Properties) { + return ((Properties) m).getProperty(name); + } + } catch (final RuntimeException unexpected) { + } + + if (m != null) { + try { + if (m instanceof LogManager) { + return ((LogManager) m).getProperty(name); + } + } catch (final LinkageError restricted) { + } catch (final RuntimeException unexpected) { + } + } + return null; + } + + /** + * Check that the current context is trusted to modify the logging + * configuration. This requires LoggingPermission("control"). + * @throws SecurityException if a security manager exists and the caller + * does not have {@code LoggingPermission("control")}. + * @since JavaMail 1.5.3 + */ + static void checkLogManagerAccess() { + boolean checked = false; + final Object m = LOG_MANAGER; + if (m != null) { + try { + if (m instanceof LogManager) { + checked = true; + ((LogManager) m).checkAccess(); + } + } catch (final SecurityException notAllowed) { + if (checked) { + throw notAllowed; + } + } catch (final LinkageError restricted) { + } catch (final RuntimeException unexpected) { + } + } + + if (!checked) { + checkLoggingAccess(); + } + } + + /** + * Check that the current context is trusted to modify the logging + * configuration when the LogManager is not present. This requires + * LoggingPermission("control"). + * @throws SecurityException if a security manager exists and the caller + * does not have {@code LoggingPermission("control")}. + * @since JavaMail 1.5.3 + */ + private static void checkLoggingAccess() { + /** + * Some environments selectively enforce logging permissions by allowing + * access to loggers but not allowing access to handlers. This is an + * indirect way of checking for LoggingPermission when the LogManager is + * not present. The root logger will lazy create handlers so the global + * logger is used instead as it is a known named logger with well + * defined behavior. If the global logger is a subclass then fallback to + * using the SecurityManager. + */ + boolean checked = false; + final Logger global = Logger.getLogger("global"); + try { + if (Logger.class == global.getClass()) { + global.removeHandler((Handler) null); + checked = true; + } + } catch (final NullPointerException unexpected) { + } + + if (!checked) { + final SecurityManager sm = System.getSecurityManager(); + if (sm != null) { + sm.checkPermission(new LoggingPermission("control", null)); + } + } + } + + /** + * Determines if access to the {@code java.util.logging.LogManager} class is + * restricted by the class loader. + * + * @return true if a LogManager is present. + * @since JavaMail 1.5.3 + */ + static boolean hasLogManager() { + final Object m = LOG_MANAGER; + return m != null && !(m instanceof Properties); + } + + /** + * Gets the ZonedDateTime from the given log record. + * + * @param record used to generate the zoned date time. + * @return null if LogRecord doesn't support nanoseconds otherwise a new + * zoned date time is returned. + * @throws NullPointerException if record is null. + * @since JavaMail 1.5.6 + */ + @SuppressWarnings("UseSpecificCatch") + static Comparable getZonedDateTime(LogRecord record) { + if (record == null) { + throw new NullPointerException(); + } + final Method m = ZDT_OF_INSTANT; + if (m != null) { + try { + return (Comparable) m.invoke((Object) null, + LR_GET_INSTANT.invoke(record), + ZI_SYSTEM_DEFAULT.invoke((Object) null)); + } catch (final RuntimeException ignore) { + assert LR_GET_INSTANT != null + && ZI_SYSTEM_DEFAULT != null : ignore; + } catch (final InvocationTargetException ite) { + final Throwable cause = ite.getCause(); + if (cause instanceof Error) { + throw (Error) cause; + } else if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } else { //Should never happen. + throw new UndeclaredThrowableException(ite); + } + } catch (final Exception ignore) { + } + } + return null; + } + + /** + * Gets the local host name from the given service. + * + * @param s the service to examine. + * @return the local host name or null. + * @throws IllegalAccessException if the method is inaccessible. + * @throws InvocationTargetException if the method throws an exception. + * @throws LinkageError if the linkage fails. + * @throws NullPointerException if the given service is null. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception if there is a problem. + * @throws NoSuchMethodException if the given service does not have a method + * to get the local host name as a string. + * @throws SecurityException if unable to inspect properties of object. + * @since JavaMail 1.5.3 + */ + static String getLocalHost(final Object s) throws Exception { + try { + final Method m = s.getClass().getMethod("getLocalHost"); + if (!Modifier.isStatic(m.getModifiers()) + && m.getReturnType() == String.class) { + return (String) m.invoke(s); + } else { + throw new NoSuchMethodException(m.toString()); + } + } catch (final ExceptionInInitializerError EIIE) { + throw wrapOrThrow(EIIE); + } catch (final InvocationTargetException ite) { + throw paramOrError(ite); + } + } + + /** + * Used to parse an ISO-8601 duration format of {@code PnDTnHnMn.nS}. + * + * @param value an ISO-8601 duration character sequence. + * @return the number of milliseconds parsed from the duration. + * @throws ClassNotFoundException if the java.time classes are not present. + * @throws IllegalAccessException if the method is inaccessible. + * @throws InvocationTargetException if the method throws an exception. + * @throws LinkageError if the linkage fails. + * @throws NullPointerException if the given duration is null. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception if there is a problem. + * @throws NoSuchMethodException if the correct time methods are missing. + * @throws SecurityException if reflective access to the java.time classes + * are not allowed. + * @since JavaMail 1.5.5 + */ + static long parseDurationToMillis(final CharSequence value) throws Exception { + try { + final Class k = findClass("java.time.Duration"); + final Method parse = k.getMethod("parse", CharSequence.class); + if (!k.isAssignableFrom(parse.getReturnType()) + || !Modifier.isStatic(parse.getModifiers())) { + throw new NoSuchMethodException(parse.toString()); + } + + final Method toMillis = k.getMethod("toMillis"); + if (!Long.TYPE.isAssignableFrom(toMillis.getReturnType()) + || Modifier.isStatic(toMillis.getModifiers())) { + throw new NoSuchMethodException(toMillis.toString()); + } + return (Long) toMillis.invoke(parse.invoke(null, value)); + } catch (final ExceptionInInitializerError EIIE) { + throw wrapOrThrow(EIIE); + } catch (final InvocationTargetException ite) { + throw paramOrError(ite); + } + } + + /** + * Converts a locale to a language tag. + * + * @param locale the locale to convert. + * @return the language tag. + * @throws NullPointerException if the given locale is null. + * @since JavaMail 1.4.5 + */ + static String toLanguageTag(final Locale locale) { + final String l = locale.getLanguage(); + final String c = locale.getCountry(); + final String v = locale.getVariant(); + final char[] b = new char[l.length() + c.length() + v.length() + 2]; + int count = l.length(); + l.getChars(0, count, b, 0); + if (c.length() != 0 || (l.length() != 0 && v.length() != 0)) { + b[count] = '-'; + ++count; //be nice to the client compiler. + c.getChars(0, c.length(), b, count); + count += c.length(); + } + + if (v.length() != 0 && (l.length() != 0 || c.length() != 0)) { + b[count] = '-'; + ++count; //be nice to the client compiler. + v.getChars(0, v.length(), b, count); + count += v.length(); + } + return String.valueOf(b, 0, count); + } + + /** + * Creates a new filter from the given class name. + * + * @param name the fully qualified class name. + * @return a new filter. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static Filter newFilter(String name) throws Exception { + return newObjectFrom(name, Filter.class); + } + + /** + * Creates a new formatter from the given class name. + * + * @param name the fully qualified class name. + * @return a new formatter. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static Formatter newFormatter(String name) throws Exception { + return newObjectFrom(name, Formatter.class); + } + + /** + * Creates a new log record comparator from the given class name. + * + * @param name the fully qualified class name. + * @return a new comparator. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + * @see java.util.logging.LogRecord + */ + @SuppressWarnings("unchecked") + static Comparator newComparator(String name) throws Exception { + return newObjectFrom(name, Comparator.class); + } + + /** + * Returns a comparator that imposes the reverse ordering of the specified + * {@link Comparator}. If the given comparator declares a public + * reverseOrder method that method is called first and the return value is + * used. If that method is not declared or the caller does not have access + * then a comparator wrapping the given comparator is returned. + * + * @param the element type to be compared + * @param c a comparator whose ordering is to be reversed by the returned + * comparator + * @return A comparator that imposes the reverse ordering of the specified + * comparator. + * @throws NullPointerException if the given comparator is null. + * @since JavaMail 1.5.0 + */ + @SuppressWarnings({"unchecked", "ThrowableResultIgnored"}) + static Comparator reverseOrder(final Comparator c) { + if (c == null) { + throw new NullPointerException(); + } + + Comparator reverse = null; + //Comparator in Java 1.8 has 'reversed' as a default method. + //This code calls that method first to allow custom + //code to define what reverse order means. + try { + //assert Modifier.isPublic(c.getClass().getModifiers()) : + // Modifier.toString(c.getClass().getModifiers()); + final Method m = c.getClass().getMethod("reversed"); + if (!Modifier.isStatic(m.getModifiers()) + && Comparator.class.isAssignableFrom(m.getReturnType())) { + try { + reverse = (Comparator) m.invoke(c); + } catch (final ExceptionInInitializerError eiie) { + throw wrapOrThrow(eiie); + } + } + } catch (final NoSuchMethodException ignore) { + } catch (final IllegalAccessException ignore) { + } catch (final RuntimeException ignore) { + } catch (final InvocationTargetException ite) { + paramOrError(ite); //Ignore invocation bugs (returned values). + } + + if (reverse == null) { + reverse = Collections.reverseOrder(c); + } + return reverse; + } + + /** + * Creates a new error manager from the given class name. + * + * @param name the fully qualified class name. + * @return a new error manager. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static ErrorManager newErrorManager(String name) throws Exception { + return newObjectFrom(name, ErrorManager.class); + } + + /** + * Determines if the given class name identifies a utility class. + * + * @param name the fully qualified class name. + * @return true if the given class name + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws SecurityException if unable to inspect properties of class. + * @since JavaMail 1.5.2 + */ + static boolean isStaticUtilityClass(String name) throws Exception { + final Class c = findClass(name); + final Class obj = Object.class; + Method[] methods; + boolean util; + if (c != obj && (methods = c.getMethods()).length != 0) { + util = true; + for (Method m : methods) { + if (m.getDeclaringClass() != obj + && !Modifier.isStatic(m.getModifiers())) { + util = false; + break; + } + } + } else { + util = false; + } + return util; + } + + /** + * Determines if the given class name is a reflection class name responsible + * for invoking methods and or constructors. + * + * @param name the fully qualified class name. + * @return true if the given class name + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws SecurityException if unable to inspect properties of class. + * @since JavaMail 1.5.2 + */ + static boolean isReflectionClass(String name) throws Exception { + String[] names = REFLECT_NAMES; + if (names == null) { //Benign data race. + REFLECT_NAMES = names = reflectionClassNames(); + } + + for (String rf : names) { //The set of names is small. + if (name.equals(rf)) { + return true; + } + } + + findClass(name); //Fail late instead of normal return. + return false; + } + + /** + * Determines all of the reflection class names used to invoke methods. + * + * This method performs indirect and direct calls on a throwable to capture + * the standard class names and the implementation class names. + * + * @return a string array containing the fully qualified class names. + * @throws Exception if there is a problem. + */ + private static String[] reflectionClassNames() throws Exception { + final Class thisClass = LogManagerProperties.class; + assert Modifier.isFinal(thisClass.getModifiers()) : thisClass; + try { + final HashSet traces = new HashSet<>(); + Throwable t = Throwable.class.getConstructor().newInstance(); + for (StackTraceElement ste : t.getStackTrace()) { + if (!thisClass.getName().equals(ste.getClassName())) { + traces.add(ste.getClassName()); + } else { + break; + } + } + + Throwable.class.getMethod("fillInStackTrace").invoke(t); + for (StackTraceElement ste : t.getStackTrace()) { + if (!thisClass.getName().equals(ste.getClassName())) { + traces.add(ste.getClassName()); + } else { + break; + } + } + return traces.toArray(new String[traces.size()]); + } catch (final InvocationTargetException ITE) { + throw paramOrError(ITE); + } + } + + /** + * Creates a new object from the given class name. + * + * @param The generic class type. + * @param name the fully qualified class name. + * @param type the assignable type for the given name. + * @return a new object assignable to the given type. + * @throws ClassCastException if class name does not match the type. + * @throws ClassNotFoundException if the class name was not found. + * @throws IllegalAccessException if the constructor is inaccessible. + * @throws InstantiationException if the given class name is abstract. + * @throws InvocationTargetException if the constructor throws an exception. + * @throws LinkageError if the linkage fails. + * @throws ExceptionInInitializerError if the static initializer fails. + * @throws Exception to match the error method of the ErrorManager. + * @throws NoSuchMethodException if the class name does not have a no + * argument constructor. + * @since JavaMail 1.4.5 + */ + static T newObjectFrom(String name, Class type) throws Exception { + try { + final Class clazz = LogManagerProperties.findClass(name); + //This check avoids additional side effects when the name parameter + //is a literal name and not a class name. + if (type.isAssignableFrom(clazz)) { + try { + return type.cast(clazz.getConstructor().newInstance()); + } catch (final InvocationTargetException ITE) { + throw paramOrError(ITE); + } + } else { + throw new ClassCastException(clazz.getName() + + " cannot be cast to " + type.getName()); + } + } catch (final NoClassDefFoundError NCDFE) { + //No class def found can occur on filesystems that are + //case insensitive (BUG ID 6196068). In some cases, we allow class + //names or literal names, this code guards against the case where a + //literal name happens to match a class name in a different case. + //This is also a nice way to adapt this error for the error manager. + throw new ClassNotFoundException(NCDFE.toString(), NCDFE); + } catch (final ExceptionInInitializerError EIIE) { + throw wrapOrThrow(EIIE); + } + } + + /** + * Returns the given exception or throws the escaping cause. + * + * @param ite any invocation target. + * @return the exception. + * @throws VirtualMachineError if present as cause. + * @throws ThreadDeath if present as cause. + * @since JavaMail 1.4.5 + */ + private static Exception paramOrError(InvocationTargetException ite) { + final Throwable cause = ite.getCause(); + if (cause != null) { + //Bitwise inclusive OR produces tighter bytecode for instanceof + //and matches with multicatch syntax. + if (cause instanceof VirtualMachineError + | cause instanceof ThreadDeath) { + throw (Error) cause; + } + } + return ite; + } + + /** + * Throws the given error if the cause is an error otherwise the given error + * is wrapped. + * + * @param eiie the error. + * @return an InvocationTargetException. + * @since JavaMail 1.5.0 + */ + private static InvocationTargetException wrapOrThrow( + ExceptionInInitializerError eiie) { + //This linkage error will escape the constructor new instance call. + //If the cause is an error, rethrow to skip any error manager. + if (eiie.getCause() instanceof Error) { + throw eiie; + } else { + //Considered a bug in the code, wrap the error so it can be + //reported to the error manager. + return new InvocationTargetException(eiie); + } + } + + /** + * This code is modified from the LogManager, which explictly states + * searching the system class loader first, then the context class loader. + * There is resistance (compatibility) to change this behavior to simply + * searching the context class loader. + * + * @param name full class name + * @return the class. + * @throws LinkageError if the linkage fails. + * @throws ClassNotFoundException if the class name was not found. + * @throws ExceptionInInitializerError if static initializer fails. + */ + private static Class findClass(String name) throws ClassNotFoundException { + ClassLoader[] loaders = getClassLoaders(); + assert loaders.length == 2 : loaders.length; + Class clazz; + if (loaders[0] != null) { + try { + clazz = Class.forName(name, false, loaders[0]); + } catch (ClassNotFoundException tryContext) { + clazz = tryLoad(name, loaders[1]); + } + } else { + clazz = tryLoad(name, loaders[1]); + } + return clazz; + } + + /** + * Loads a class using the given loader or the class loader of this class. + * + * @param name the class name. + * @param l any class loader or null. + * @return the raw class. + * @throws ClassNotFoundException if not found. + */ + private static Class tryLoad(String name, ClassLoader l) throws ClassNotFoundException { + if (l != null) { + return Class.forName(name, false, l); + } else { + return Class.forName(name); + } + } + + /** + * Gets the class loaders using elevated privileges. + * + * @return any array of class loaders. Indexes may be null. + */ + private static ClassLoader[] getClassLoaders() { + return AccessController.doPrivileged(new PrivilegedAction() { + + @SuppressWarnings("override") //JDK-6954234 + public ClassLoader[] run() { + final ClassLoader[] loaders = new ClassLoader[2]; + try { + loaders[0] = ClassLoader.getSystemClassLoader(); + } catch (SecurityException ignore) { + loaders[0] = null; + } + + try { + loaders[1] = Thread.currentThread().getContextClassLoader(); + } catch (SecurityException ignore) { + loaders[1] = null; + } + return loaders; + } + }); + } + /** + * The namespace prefix to search LogManager and defaults. + */ + private final String prefix; + + /** + * Creates a log manager properties object. + * + * @param parent the parent properties. + * @param prefix the namespace prefix. + * @throws NullPointerException if prefix or + * parent is null. + */ + LogManagerProperties(final Properties parent, final String prefix) { + super(parent); + if (parent == null || prefix == null) { + throw new NullPointerException(); + } + this.prefix = prefix; + } + + /** + * Returns a properties object that contains a snapshot of the current + * state. This method violates the clone contract so that no instances of + * LogManagerProperties is exported for public use. + * + * @return the snapshot. + * @since JavaMail 1.4.4 + */ + @Override + @SuppressWarnings("CloneDoesntCallSuperClone") + public synchronized Object clone() { + return exportCopy(defaults); + } + + /** + * Searches defaults, then searches the log manager if available or the + * system properties by the prefix property, and then by the key itself. + * + * @param key a non null key. + * @return the value for that key. + */ + @Override + public synchronized String getProperty(final String key) { + String value = defaults.getProperty(key); + if (value == null) { + if (key.length() > 0) { + value = fromLogManager(prefix + '.' + key); + } + + if (value == null) { + value = fromLogManager(key); + } + + /** + * Copy the log manager properties as we read them. If a value is no + * longer present in the LogManager read it from here. The reason + * this works is because LogManager.reset() closes all attached + * handlers therefore, stale values only exist in closed handlers. + */ + if (value != null) { + super.put(key, value); + } else { + Object v = super.get(key); //defaults are not used. + value = v instanceof String ? (String) v : null; + } + } + return value; + } + + /** + * Calls getProperty directly. If getProperty returns null the default value + * is returned. + * + * @param key a key to search for. + * @param def the default value to use if not found. + * @return the value for the key. + * @since JavaMail 1.4.4 + */ + @Override + public String getProperty(final String key, final String def) { + final String value = this.getProperty(key); + return value == null ? def : value; + } + + /** + * Required to work with PropUtil. Calls getProperty directly if the given + * key is a string. Otherwise, performs a get operation on the defaults + * followed by the normal hash table get. + * + * @param key any key. + * @return the value for the key or null. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized Object get(final Object key) { + Object value; + if (key instanceof String) { + value = getProperty((String) key); + } else { + value = null; + } + + //Search for non-string value. + if (value == null) { + value = defaults.get(key); + if (value == null && !defaults.containsKey(key)) { + value = super.get(key); + } + } + return value; + } + + /** + * Required to work with PropUtil. An updated copy of the key is fetched + * from the log manager if the key doesn't exist in this properties. + * + * @param key any key. + * @return the value for the key or the default value for the key. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized Object put(final Object key, final Object value) { + if (key instanceof String && value instanceof String) { + final Object def = preWrite(key); + final Object man = super.put(key, value); + return man == null ? def : man; + } else { + return super.put(key, value); + } + } + + /** + * Calls the put method directly. + * + * @param key any key. + * @return the value for the key or the default value for the key. + * @since JavaMail 1.4.5 + */ + @Override + public Object setProperty(String key, String value) { + return this.put(key, value); + } + + /** + * Required to work with PropUtil. An updated copy of the key is fetched + * from the log manager prior to returning. + * + * @param key any key. + * @return the value for the key or null. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized boolean containsKey(final Object key) { + boolean found = key instanceof String + && getProperty((String) key) != null; + if (!found) { + found = defaults.containsKey(key) || super.containsKey(key); + } + return found; + } + + /** + * Required to work with PropUtil. An updated copy of the key is fetched + * from the log manager if the key doesn't exist in this properties. + * + * @param key any key. + * @return the value for the key or the default value for the key. + * @since JavaMail 1.4.5 + */ + @Override + public synchronized Object remove(final Object key) { + final Object def = preWrite(key); + final Object man = super.remove(key); + return man == null ? def : man; + } + + /** + * It is assumed that this method will never be called. No way to get the + * property names from LogManager. + * + * @return the property names + */ + @Override + public Enumeration propertyNames() { + assert false; + return super.propertyNames(); + } + + /** + * It is assumed that this method will never be called. The prefix value is + * not used for the equals method. + * + * @param o any object or null. + * @return true if equal, otherwise false. + */ + @Override + public boolean equals(final Object o) { + if (o == null) { + return false; + } + if (o == this) { + return true; + } + if (o instanceof Properties == false) { + return false; + } + assert false : prefix; + return super.equals(o); + } + + /** + * It is assumed that this method will never be called. See equals. + * + * @return the hash code. + */ + @Override + public int hashCode() { + assert false : prefix.hashCode(); + return super.hashCode(); + } + + /** + * Called before a write operation of a key. Caches a key read from the log + * manager in this properties object. The key is only cached if it is an + * instance of a String and this properties doesn't contain a copy of the + * key. + * + * @param key the key to search. + * @return the default value for the key. + */ + private Object preWrite(final Object key) { + assert Thread.holdsLock(this); + return get(key); + } + + /** + * Creates a public snapshot of this properties object using the given + * parent properties. + * + * @param parent the defaults to use with the snapshot. + * @return the safe snapshot. + */ + private Properties exportCopy(final Properties parent) { + Thread.holdsLock(this); + final Properties child = new Properties(parent); + child.putAll(this); + return child; + } + + /** + * It is assumed that this method will never be called. We return a safe + * copy for export to avoid locking this properties object or the defaults + * during write. + * + * @return the parent properties. + * @throws ObjectStreamException if there is a problem. + */ + private synchronized Object writeReplace() throws ObjectStreamException { + assert false; + return exportCopy((Properties) defaults.clone()); + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/MailHandler.java b/app/src/main/java/com/sun/mail/util/logging/MailHandler.java new file mode 100644 index 0000000000..ecfe0684a4 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/MailHandler.java @@ -0,0 +1,4416 @@ +/* + * Copyright (c) 2009, 2020 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2009, 2020 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ + +package com.sun.mail.util.logging; + +import static com.sun.mail.util.logging.LogManagerProperties.fromLogManager; +import java.io.*; +import java.lang.reflect.InvocationTargetException; +import java.net.InetAddress; +import java.net.URLConnection; +import java.net.UnknownHostException; +import java.nio.charset.Charset; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.*; +import java.util.logging.*; +import java.util.logging.Formatter; +import javax.activation.*; +import javax.mail.*; +import javax.mail.internet.*; +import javax.mail.util.ByteArrayDataSource; + +/** + * Handler that formats log records as an email message. + * + *

+ * This Handler will store a fixed number of log records used to + * generate a single email message. When the internal buffer reaches capacity, + * all log records are formatted and placed in an email which is sent to an + * email server. The code to manually setup this handler can be as simple as + * the following: + * + *

+ *      Properties props = new Properties();
+ *      props.put("mail.smtp.host", "my-mail-server");
+ *      props.put("mail.to", "me@example.com");
+ *      props.put("verify", "local");
+ *      MailHandler h = new MailHandler(props);
+ *      h.setLevel(Level.WARNING);
+ * 
+ * + *

+ * Configuration: + * The LogManager should define at least one or more recipient addresses and a + * mail host for outgoing email. The code to setup this handler via the + * logging properties can be as simple as the following: + * + *

+ *      #Default MailHandler settings.
+ *      com.sun.mail.util.logging.MailHandler.mail.smtp.host = my-mail-server
+ *      com.sun.mail.util.logging.MailHandler.mail.to = me@example.com
+ *      com.sun.mail.util.logging.MailHandler.level = WARNING
+ *      com.sun.mail.util.logging.MailHandler.verify = local
+ * 
+ * + * For a custom handler, e.g. com.foo.MyHandler, the properties + * would be: + * + *
+ *      #Subclass com.foo.MyHandler settings.
+ *      com.foo.MyHandler.mail.smtp.host = my-mail-server
+ *      com.foo.MyHandler.mail.to = me@example.com
+ *      com.foo.MyHandler.level = WARNING
+ *      com.foo.MyHandler.verify = local
+ * 
+ * + * All mail properties documented in the Java Mail API cascade to + * the LogManager by prefixing a key using the fully qualified class name of + * this MailHandler or the fully qualified derived class name dot + * mail property. If the prefixed property is not found, then the mail property + * itself is searched in the LogManager. By default each + * MailHandler is initialized using the following LogManager + * configuration properties where <handler-name> refers to + * the fully qualified class name of the handler. If properties are not + * defined, or contain invalid values, then the specified default values are + * used. + * + *
    + *
  • <handler-name>.attachment.filters a comma + * separated list of Filter class names used to create each + * attachment. The literal null is reserved for attachments that + * do not require filtering. (defaults to the + * {@linkplain java.util.logging.Handler#getFilter() body} filter) + * + *
  • <handler-name>.attachment.formatters a comma + * separated list of Formatter class names used to create each + * attachment. (default is no attachments) + * + *
  • <handler-name>.attachment.names a comma separated + * list of names or Formatter class names of each attachment. All + * control characters are removed from the attachment names. + * (default is {@linkplain java.util.logging.Formatter#toString() toString} + * of the attachment formatter) + * + *
  • <handler-name>.authenticator name of an + * {@linkplain javax.mail.Authenticator} class used to provide login credentials + * to the email server or string literal that is the password used with the + * {@linkplain Authenticator#getDefaultUserName() default} user name. + * (default is null) + * + *
  • <handler-name>.capacity the max number of + * LogRecord objects include in each email message. + * (defaults to 1000) + * + *
  • <handler-name>.comparator name of a + * {@linkplain java.util.Comparator} class used to sort the published + * LogRecord objects prior to all formatting. + * (defaults to null meaning records are unsorted). + * + *
  • <handler-name>.comparator.reverse a boolean + * true to reverse the order of the specified comparator or + * false to retain the original order. + * (defaults to false) + * + *
  • <handler-name>.encoding the name of the Java + * {@linkplain java.nio.charset.Charset#name() character set} to use for the + * email message. (defaults to null, the + * {@linkplain javax.mail.internet.MimeUtility#getDefaultJavaCharset() default} + * platform encoding). + * + *
  • <handler-name>.errorManager name of an + * ErrorManager class used to handle any configuration or mail + * transport problems. (defaults to java.util.logging.ErrorManager) + * + *
  • <handler-name>.filter name of a Filter + * class used for the body of the message. (defaults to null, + * allow all records) + * + *
  • <handler-name>.formatter name of a + * Formatter class used to format the body of this message. + * (defaults to java.util.logging.SimpleFormatter) + * + *
  • <handler-name>.level specifies the default level + * for this Handler (defaults to Level.WARNING). + * + *
  • <handler-name>.mail.bcc a comma separated list of + * addresses which will be blind carbon copied. Typically, this is set to the + * recipients that may need to be privately notified of a log message or + * notified that a log message was sent to a third party such as a support team. + * The empty string can be used to specify no blind carbon copied address. + * (defaults to null, none) + * + *
  • <handler-name>.mail.cc a comma separated list of + * addresses which will be carbon copied. Typically, this is set to the + * recipients that may need to be notified of a log message but, are not + * required to provide direct support. The empty string can be used to specify + * no carbon copied address. (defaults to null, none) + * + *
  • <handler-name>.mail.from a comma separated list of + * addresses which will be from addresses. Typically, this is set to the email + * address identifying the user running the application. The empty string can + * be used to override the default behavior and specify no from address. + * (defaults to the {@linkplain javax.mail.Message#setFrom() local address}) + * + *
  • <handler-name>.mail.host the host name or IP + * address of the email server. (defaults to null, use + * {@linkplain Transport#protocolConnect default} + * Java Mail behavior) + * + *
  • <handler-name>.mail.reply.to a comma separated + * list of addresses which will be reply-to addresses. Typically, this is set + * to the recipients that provide support for the application itself. The empty + * string can be used to specify no reply-to address. + * (defaults to null, none) + * + *
  • <handler-name>.mail.to a comma separated list of + * addresses which will be send-to addresses. Typically, this is set to the + * recipients that provide support for the application, system, and/or + * supporting infrastructure. The empty string can be used to specify no + * send-to address which overrides the default behavior. (defaults to + * {@linkplain javax.mail.internet.InternetAddress#getLocalAddress + * local address}.) + * + *
  • <handler-name>.mail.sender a single address + * identifying sender of the email; never equal to the from address. Typically, + * this is set to the email address identifying the application itself. The + * empty string can be used to specify no sender address. + * (defaults to null, none) + * + *
  • <handler-name>.subject the name of a + * Formatter class or string literal used to create the subject + * line. The empty string can be used to specify no subject. All control + * characters are removed from the subject line. (defaults to {@linkplain + * com.sun.mail.util.logging.CollectorFormatter CollectorFormatter}.) + * + *
  • <handler-name>.pushFilter the name of a + * Filter class used to trigger an early push. + * (defaults to null, no early push) + * + *
  • <handler-name>.pushLevel the level which will + * trigger an early push. (defaults to Level.OFF, only push when + * full) + * + *
  • <handler-name>.verify used to + * verify the Handler configuration prior to a push. + *
      + *
    • If the value is not set, equal to an empty string, or equal to the + * literal null then no settings are verified prior to a push. + *
    • If set to a value of limited then the + * Handler will verify minimal local machine settings. + *
    • If set to a value of local the Handler + * will verify all of settings of the local machine. + *
    • If set to a value of resolve, the Handler + * will verify all local settings and try to resolve the remote host name + * with the domain name server. + *
    • If set to a value of login, the Handler + * will verify all local settings and try to establish a connection with + * the email server. + *
    • If set to a value of remote, the Handler + * will verify all local settings, try to establish a connection with the + * email server, and try to verify the envelope of the email message. + *
    + * If this Handler is only implicitly closed by the + * LogManager, then verification should be turned on. + * (defaults to null, no verify). + *
+ * + *

+ * Normalization: + * The error manager, filters, and formatters when loaded from the LogManager + * are converted into canonical form inside the MailHandler. The pool of + * interned values is limited to each MailHandler object such that no two + * MailHandler objects created by the LogManager will be created sharing + * identical error managers, filters, or formatters. If a filter or formatter + * should not be interned then it is recommended to retain the identity + * equals and identity hashCode methods as the implementation. For a filter or + * formatter to be interned the class must implement the + * {@linkplain java.lang.Object#equals(java.lang.Object) equals} + * and {@linkplain java.lang.Object#hashCode() hashCode} methods. + * The recommended code to use for stateless filters and formatters is: + *

+ * public boolean equals(Object obj) {
+ *     return obj == null ? false : obj.getClass() == getClass();
+ * }
+ *
+ * public int hashCode() {
+ *     return 31 * getClass().hashCode();
+ * }
+ * 
+ * + *

+ * Sorting: + * All LogRecord objects are ordered prior to formatting if this + * Handler has a non null comparator. Developers might be + * interested in sorting the formatted email by thread id, time, and sequence + * properties of a LogRecord. Where as system administrators might + * be interested in sorting the formatted email by thrown, level, time, and + * sequence properties of a LogRecord. If comparator for this + * handler is null then the order is unspecified. + * + *

+ * Formatting: + * The main message body is formatted using the Formatter returned + * by getFormatter(). Only records that pass the filter returned + * by getFilter() will be included in the message body. The + * subject Formatter will see all LogRecord objects + * that were published regardless of the current Filter. The MIME + * type of the message body can be + * {@linkplain FileTypeMap#setDefaultFileTypeMap overridden} + * by adding a MIME {@linkplain MimetypesFileTypeMap entry} using the simple + * class name of the body formatter as the file extension. The MIME type of the + * attachments can be overridden by changing the attachment file name extension + * or by editing the default MIME entry for a specific file name extension. + * + *

+ * Attachments: + * This Handler allows multiple attachments per each email message. + * The presence of an attachment formatter will change the content type of the + * email message to a multi-part message. The attachment order maps directly to + * the array index order in this Handler with zero index being the + * first attachment. The number of attachment formatters controls the number of + * attachments per email and the content type of each attachment. The + * attachment filters determine if a LogRecord will be included in + * an attachment. If an attachment filter is null then all records + * are included for that attachment. Attachments without content will be + * omitted from email message. The attachment name formatters create the file + * name for an attachment. Custom attachment name formatters can be used to + * generate an attachment name based on the contents of the attachment. + * + *

+ * Push Level and Push Filter: + * The push method, push level, and optional push filter can be used to + * conditionally trigger a push at or prior to full capacity. When a push + * occurs, the current buffer is formatted into an email and is sent to the + * email server. If the push method, push level, or push filter trigger a push + * then the outgoing email is flagged as high importance with urgent priority. + * + *

+ * Buffering: + * Log records that are published are stored in an internal buffer. When this + * buffer reaches capacity the existing records are formatted and sent in an + * email. Any published records can be sent before reaching capacity by + * explictly calling the flush, push, or + * close methods. If a circular buffer is required then this + * handler can be wrapped with a {@linkplain java.util.logging.MemoryHandler} + * typically with an equivalent capacity, level, and push level. + * + *

+ * Error Handling: + * If the transport of an email message fails, the email is converted to + * a {@linkplain javax.mail.internet.MimeMessage#writeTo raw} + * {@linkplain java.io.ByteArrayOutputStream#toString(java.lang.String) string} + * and is then passed as the msg parameter to + * {@linkplain Handler#reportError reportError} along with the exception + * describing the cause of the failure. This allows custom error managers to + * store, {@linkplain javax.mail.internet.MimeMessage#MimeMessage( + * javax.mail.Session, java.io.InputStream) reconstruct}, and resend the + * original MimeMessage. The message parameter string is not a raw email + * if it starts with value returned from Level.SEVERE.getName(). + * Custom error managers can use the following test to determine if the + * msg parameter from this handler is a raw email: + * + *

+ * public void error(String msg, Exception ex, int code) {
+ *      if (msg == null || msg.length() == 0 || msg.startsWith(Level.SEVERE.getName())) {
+ *          super.error(msg, ex, code);
+ *      } else {
+ *          //The 'msg' parameter is a raw email.
+ *      }
+ * }
+ * 
+ * + * @author Jason Mehrens + * @since JavaMail 1.4.3 + */ +public class MailHandler extends Handler { + /** + * Use the emptyFilterArray method. + */ + private static final Filter[] EMPTY_FILTERS = new Filter[0]; + /** + * Use the emptyFormatterArray method. + */ + private static final Formatter[] EMPTY_FORMATTERS = new Formatter[0]; + /** + * Min byte size for header data. Used for initial arrays sizing. + */ + private static final int MIN_HEADER_SIZE = 1024; + /** + * Cache the off value. + */ + private static final int offValue = Level.OFF.intValue(); + /** + * The action to set the context class loader for use with the Jakarta Mail API. + * Load and pin this before it is loaded in the close method. The field is + * declared as java.security.PrivilegedAction so + * WebappClassLoader.clearReferencesStaticFinal() method will ignore this + * field. + */ + private static final PrivilegedAction MAILHANDLER_LOADER + = new GetAndSetContext(MailHandler.class); + /** + * A thread local mutex used to prevent logging loops. This code has to be + * prepared to deal with unexpected null values since the + * WebappClassLoader.clearReferencesThreadLocals() and + * InnocuousThread.eraseThreadLocals() can remove thread local values. + * The MUTEX has 5 states: + * 1. A null value meaning default state of not publishing. + * 2. MUTEX_PUBLISH on first entry of a push or publish. + * 3. The index of the first filter to accept a log record. + * 4. MUTEX_REPORT when cycle of records is detected. + * 5. MUTEXT_LINKAGE when a linkage error is reported. + */ + private static final ThreadLocal MUTEX = new ThreadLocal<>(); + /** + * The marker object used to report a publishing state. + * This must be less than the body filter index (-1). + */ + private static final Integer MUTEX_PUBLISH = -2; + /** + * The used for the error reporting state. + * This must be less than the PUBLISH state. + */ + private static final Integer MUTEX_REPORT = -4; + /** + * The used for linkage error reporting. + * This must be less than the REPORT state. + */ + private static final Integer MUTEX_LINKAGE = -8; + /** + * Used to turn off security checks. + */ + private volatile boolean sealed; + /** + * Determines if we are inside of a push. + * Makes the handler properties read-only during a push. + */ + private boolean isWriting; + /** + * Holds all of the email server properties. + */ + private Properties mailProps; + /** + * Holds the authenticator required to login to the email server. + */ + private Authenticator auth; + /** + * Holds the session object used to generate emails. + * Sessions can be shared by multiple threads. + * See JDK-6228391 and K 6278. + */ + private Session session; + /** + * A mapping of log record to matching filter index. Negative one is used + * to track the body filter. Zero and greater is used to track the + * attachment parts. All indexes less than or equal to the matched value + * have already seen the given log record. + */ + private int[] matched; + /** + * Holds all of the log records that will be used to create the email. + */ + private LogRecord[] data; + /** + * The number of log records in the buffer. + */ + private int size; + /** + * The maximum number of log records to format per email. + * Used to roughly bound the size of an email. + * Every time the capacity is reached, the handler will push. + * The capacity will be negative if this handler is closed. + * Negative values are used to ensure all records are pushed. + */ + private int capacity; + /** + * Used to order all log records prior to formatting. The main email body + * and all attachments use the order determined by this comparator. If no + * comparator is present the log records will be in no specified order. + */ + private Comparator comparator; + /** + * Holds the formatter used to create the subject line of the email. + * A subject formatter is not required for the email message. + * All published records pass through the subject formatter. + */ + private Formatter subjectFormatter; + /** + * Holds the push level for this handler. + * This is only required if an email must be sent prior to shutdown + * or before the buffer is full. + */ + private Level pushLevel; + /** + * Holds the push filter for trigger conditions requiring an early push. + * Only gets called if the given log record is greater than or equal + * to the push level and the push level is not Level.OFF. + */ + private Filter pushFilter; + /** + * Holds the entry and body filter for this handler. + * There is no way to un-seal the super handler. + */ + private volatile Filter filter; + /** + * Holds the level for this handler. + * There is no way to un-seal the super handler. + */ + private volatile Level logLevel = Level.ALL; + /** + * Holds the filters for each attachment. Filters are optional for + * each attachment. This is declared volatile because this is treated as + * copy-on-write. The VO_VOLATILE_REFERENCE_TO_ARRAY warning is a false + * positive. + */ + @SuppressWarnings("VolatileArrayField") + private volatile Filter[] attachmentFilters; + /** + * Holds the encoding name for this handler. + * There is no way to un-seal the super handler. + */ + private String encoding; + /** + * Holds the entry and body filter for this handler. + * There is no way to un-seal the super handler. + */ + private Formatter formatter; + /** + * Holds the formatters that create the content for each attachment. + * Each formatter maps directly to an attachment. The formatters + * getHead, format, and getTail methods are only called if one or more + * log records pass through the attachment filters. + */ + private Formatter[] attachmentFormatters; + /** + * Holds the formatters that create the file name for each attachment. + * Each formatter must produce a non null and non empty name. + * The final file name will be the concatenation of one getHead call, plus + * all of the format calls, plus one getTail call. + */ + private Formatter[] attachmentNames; + /** + * Used to override the content type for the body and set the content type + * for each attachment. + */ + private FileTypeMap contentTypes; + /** + * Holds the error manager for this handler. + * There is no way to un-seal the super handler. + */ + private volatile ErrorManager errorManager = defaultErrorManager(); + + /** + * Creates a MailHandler that is configured by the + * LogManager configuration properties. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + public MailHandler() { + init((Properties) null); + sealed = true; + checkAccess(); + } + + /** + * Creates a MailHandler that is configured by the + * LogManager configuration properties but overrides the + * LogManager capacity with the given capacity. + * @param capacity of the internal buffer. + * @throws IllegalArgumentException if capacity less than one. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + public MailHandler(final int capacity) { + init((Properties) null); + sealed = true; + setCapacity0(capacity); + } + + /** + * Creates a mail handler with the given mail properties. + * The key/value pairs are defined in the Java Mail API + * documentation. This Handler will also search the + * LogManager for defaults if needed. + * @param props a non null properties object. + * @throws NullPointerException if props is null. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + public MailHandler(final Properties props) { + if (props == null) { + throw new NullPointerException(); + } + init(props); + sealed = true; + setMailProperties0(props); + } + + /** + * Check if this Handler would actually log a given + * LogRecord into its internal buffer. + *

+ * This method checks if the LogRecord has an appropriate level + * and whether it satisfies any Filter including any + * attachment filters. + * However it does not check whether the LogRecord would + * result in a "push" of the buffer contents. + *

+ * @param record a LogRecord or null. + * @return true if the LogRecord would be logged. + */ + @Override + public boolean isLoggable(final LogRecord record) { + if (record == null) { //JDK-8233979 + return false; + } + + int levelValue = getLevel().intValue(); + if (record.getLevel().intValue() < levelValue || levelValue == offValue) { + return false; + } + + Filter body = getFilter(); + if (body == null || body.isLoggable(record)) { + setMatchedPart(-1); + return true; + } + + return isAttachmentLoggable(record); + } + + /** + * Stores a LogRecord in the internal buffer. + *

+ * The isLoggable method is called to check if the given log + * record is loggable. If the given record is loggable, it is copied into + * an internal buffer. Then the record's level property is compared with + * the push level. If the given level of the LogRecord + * is greater than or equal to the push level then the push filter is + * called. If no push filter exists, the push filter returns true, + * or the capacity of the internal buffer has been reached then all buffered + * records are formatted into one email and sent to the server. + * + * @param record description of the log event or null. + */ + @Override + public void publish(final LogRecord record) { + /** + * It is possible for the handler to be closed after the + * call to isLoggable. In that case, the current thread + * will push to ensure that all published records are sent. + * See close(). + */ + + if (tryMutex()) { + try { + if (isLoggable(record)) { + if (record != null) { + record.getSourceMethodName(); //Infer caller. + publish0(record); + } else { //Override of isLoggable is broken. + reportNullError(ErrorManager.WRITE_FAILURE); + } + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, ErrorManager.WRITE_FAILURE); + } finally { + releaseMutex(); + } + } else { + reportUnPublishedError(record); + } + } + + /** + * Performs the publish after the record has been filtered. + * @param record the record which must not be null. + * @since JavaMail 1.4.5 + */ + private void publish0(final LogRecord record) { + Message msg; + boolean priority; + synchronized (this) { + if (size == data.length && size < capacity) { + grow(); + } + + if (size < data.length) { + //assert data.length == matched.length; + matched[size] = getMatchedPart(); + data[size] = record; + ++size; //Be nice to client compiler. + priority = isPushable(record); + if (priority || size >= capacity) { + msg = writeLogRecords(ErrorManager.WRITE_FAILURE); + } else { + msg = null; + } + } else { + priority = false; + msg = null; + } + } + + if (msg != null) { + send(msg, priority, ErrorManager.WRITE_FAILURE); + } + } + + /** + * Report to the error manager that a logging loop was detected and + * we are going to break the cycle of messages. It is possible that + * a custom error manager could continue the cycle in which case + * we will stop trying to report errors. + * @param record the record or null. + * @since JavaMail 1.4.6 + */ + private void reportUnPublishedError(LogRecord record) { + final Integer idx = MUTEX.get(); + if (idx == null || idx > MUTEX_REPORT) { + MUTEX.set(MUTEX_REPORT); + try { + final String msg; + if (record != null) { + final Formatter f = createSimpleFormatter(); + msg = "Log record " + record.getSequenceNumber() + + " was not published. " + + head(f) + format(f, record) + tail(f, ""); + } else { + msg = null; + } + Exception e = new IllegalStateException( + "Recursive publish detected by thread " + + Thread.currentThread()); + reportError(msg, e, ErrorManager.WRITE_FAILURE); + } finally { + if (idx != null) { + MUTEX.set(idx); + } else { + MUTEX.remove(); + } + } + } + } + + /** + * Used to detect reentrance by the current thread to the publish method. + * This mutex is thread local scope and will not block other threads. + * The state is advanced on if the current thread is in a reset state. + * @return true if the mutex was acquired. + * @since JavaMail 1.4.6 + */ + private boolean tryMutex() { + if (MUTEX.get() == null) { + MUTEX.set(MUTEX_PUBLISH); + return true; + } else { + return false; + } + } + + /** + * Releases the mutex held by the current thread. + * This mutex is thread local scope and will not block other threads. + * @since JavaMail 1.4.6 + */ + private void releaseMutex() { + MUTEX.remove(); + } + + /** + * This is used to get the filter index from when {@code isLoggable} and + * {@code isAttachmentLoggable} was invoked by {@code publish} method. + * + * @return the filter index or MUTEX_PUBLISH if unknown. + * @since JavaMail 1.5.5 + * @throws NullPointerException if tryMutex was not called. + */ + private int getMatchedPart() { + //assert Thread.holdsLock(this); + Integer idx = MUTEX.get(); + if (idx == null || idx >= readOnlyAttachmentFilters().length) { + idx = MUTEX_PUBLISH; + } + return idx; + } + + /** + * This is used to record the filter index when {@code isLoggable} and + * {@code isAttachmentLoggable} was invoked by {@code publish} method. + * + * @param index the filter index. + * @since JavaMail 1.5.5 + */ + private void setMatchedPart(int index) { + if (MUTEX_PUBLISH.equals(MUTEX.get())) { + MUTEX.set(index); + } + } + + /** + * Clear previous matches when the filters are modified and there are + * existing log records that were matched. + * @param index the lowest filter index to clear. + * @since JavaMail 1.5.5 + */ + private void clearMatches(int index) { + assert Thread.holdsLock(this); + for (int r = 0; r < size; ++r) { + if (matched[r] >= index) { + matched[r] = MUTEX_PUBLISH; + } + } + } + + /** + * A callback method for when this object is about to be placed into + * commission. This contract is defined by the + * {@code org.glassfish.hk2.api.PostConstruct} interface. If this class is + * loaded via a lifecycle managed environment other than HK2 then it is + * recommended that this method is called either directly or through + * extending this class to signal that this object is ready for use. + * + * @since JavaMail 1.5.3 + */ + //@javax.annotation.PostConstruct + public void postConstruct() { + } + + /** + * A callback method for when this object is about to be decommissioned. + * This contract is defined by the {@code org.glassfish.hk2.api.PreDestory} + * interface. If this class is loaded via a lifecycle managed environment + * other than HK2 then it is recommended that this method is called either + * directly or through extending this class to signal that this object will + * be destroyed. + * + * @since JavaMail 1.5.3 + */ + //@javax.annotation.PreDestroy + public void preDestroy() { + /** + * Close can require permissions so just trigger a push. + */ + push(false, ErrorManager.CLOSE_FAILURE); + } + + /** + * Pushes any buffered records to the email server as high importance with + * urgent priority. The internal buffer is then cleared. Does nothing if + * called from inside a push. + * @see #flush() + */ + public void push() { + push(true, ErrorManager.FLUSH_FAILURE); + } + + /** + * Pushes any buffered records to the email server as normal priority. + * The internal buffer is then cleared. Does nothing if called from inside + * a push. + * @see #push() + */ + @Override + public void flush() { + push(false, ErrorManager.FLUSH_FAILURE); + } + + /** + * Prevents any other records from being published. + * Pushes any buffered records to the email server as normal priority. + * The internal buffer is then cleared. Once this handler is closed it + * will remain closed. + *

+ * If this Handler is only implicitly closed by the + * LogManager, then verification should + * be turned on. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @see #flush() + */ + @Override + public void close() { + try { + checkAccess(); //Ensure setLevel works before clearing the buffer. + Message msg = null; + synchronized (this) { + try { + msg = writeLogRecords(ErrorManager.CLOSE_FAILURE); + } finally { //Change level after formatting. + this.logLevel = Level.OFF; + /** + * The sign bit of the capacity is set to ensure that + * records that have passed isLoggable, but have yet to be + * added to the internal buffer, are immediately pushed as + * an email. + */ + if (this.capacity > 0) { + this.capacity = -this.capacity; + } + + //Ensure not inside a push. + if (size == 0 && data.length != 1) { + this.data = new LogRecord[1]; + this.matched = new int[this.data.length]; + } + } + } + + if (msg != null) { + send(msg, false, ErrorManager.CLOSE_FAILURE); + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, ErrorManager.CLOSE_FAILURE); + } + } + + /** + * Set the log level specifying which message levels will be + * logged by this Handler. Message levels lower than this + * value will be discarded. + * @param newLevel the new value for the log level + * @throws NullPointerException if newLevel is + * null. + * @throws SecurityException if a security manager exists and + * the caller does not have + * LoggingPermission("control"). + */ + @Override + public void setLevel(final Level newLevel) { + if (newLevel == null) { + throw new NullPointerException(); + } + checkAccess(); + + //Don't allow a closed handler to be opened (half way). + synchronized (this) { //Wait for writeLogRecords. + if (this.capacity > 0) { + this.logLevel = newLevel; + } + } + } + + /** + * Get the log level specifying which messages will be logged by this + * Handler. Message levels lower than this level will be + * discarded. + * + * @return the level of messages being logged. + */ + @Override + public Level getLevel() { + return logLevel; //Volatile access. + } + + /** + * Retrieves the ErrorManager for this Handler. + * + * @return the ErrorManager for this Handler + * @throws SecurityException if a security manager exists and if the caller + * does not have LoggingPermission("control"). + */ + @Override + public ErrorManager getErrorManager() { + checkAccess(); + return this.errorManager; //Volatile access. + } + + /** + * Define an ErrorManager for this Handler. + *

+ * The ErrorManager's "error" method will be invoked if any errors occur + * while using this Handler. + * + * @param em the new ErrorManager + * @throws SecurityException if a security manager exists and if the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if the given error manager is null. + */ + @Override + public void setErrorManager(final ErrorManager em) { + checkAccess(); + setErrorManager0(em); + } + + /** + * Sets the error manager on this handler and the super handler. In secure + * environments the super call may not be allowed which is not a failure + * condition as it is an attempt to free the unused handler error manager. + * + * @param em a non null error manager. + * @throws NullPointerException if the given error manager is null. + * @since JavaMail 1.5.6 + */ + private void setErrorManager0(final ErrorManager em) { + if (em == null) { + throw new NullPointerException(); + } + try { + synchronized (this) { //Wait for writeLogRecords. + this.errorManager = em; + super.setErrorManager(em); //Try to free super error manager. + } + } catch (RuntimeException | LinkageError ignore) { + } + } + + /** + * Get the current Filter for this Handler. + * + * @return a Filter object (may be null) + */ + @Override + public Filter getFilter() { + return this.filter; //Volatile access. + } + + /** + * Set a Filter to control output on this Handler. + *

+ * For each call of publish the Handler will call + * this Filter (if it is non-null) to check if the + * LogRecord should be published or discarded. + * + * @param newFilter a Filter object (may be null) + * @throws SecurityException if a security manager exists and if the caller + * does not have LoggingPermission("control"). + */ + @Override + public void setFilter(final Filter newFilter) { + checkAccess(); + synchronized (this) { //Wait for writeLogRecords. + if (newFilter != filter) { + clearMatches(-1); + } + this.filter = newFilter; //Volatile access. + } + } + + /** + * Return the character encoding for this Handler. + * + * @return The encoding name. May be null, which indicates the default + * encoding should be used. + */ + @Override + public synchronized String getEncoding() { + return this.encoding; + } + + /** + * Set the character encoding used by this Handler. + *

+ * The encoding should be set before any LogRecords are written + * to the Handler. + * + * @param encoding The name of a supported character encoding. May be + * null, to indicate the default platform encoding. + * @throws SecurityException if a security manager exists and if the caller + * does not have LoggingPermission("control"). + * @throws UnsupportedEncodingException if the named encoding is not + * supported. + */ + @Override + public void setEncoding(String encoding) throws UnsupportedEncodingException { + checkAccess(); + setEncoding0(encoding); + } + + /** + * Set the character encoding used by this handler. This method does not + * check permissions of the caller. + * + * @param e any encoding name or null for the default. + * @throws UnsupportedEncodingException if the given encoding is not supported. + */ + private void setEncoding0(String e) throws UnsupportedEncodingException { + if (e != null) { + try { + if (!java.nio.charset.Charset.isSupported(e)) { + throw new UnsupportedEncodingException(e); + } + } catch (java.nio.charset.IllegalCharsetNameException icne) { + throw new UnsupportedEncodingException(e); + } + } + + synchronized (this) { //Wait for writeLogRecords. + this.encoding = e; + } + } + + /** + * Return the Formatter for this Handler. + * + * @return the Formatter (may be null). + */ + @Override + public synchronized Formatter getFormatter() { + return this.formatter; + } + + /** + * Set a Formatter. This Formatter will be used + * to format LogRecords for this Handler. + *

+ * Some Handlers may not use Formatters, in which + * case the Formatter will be remembered, but not used. + *

+ * @param newFormatter the Formatter to use (may not be null) + * @throws SecurityException if a security manager exists and if the caller + * does not have LoggingPermission("control"). + * @throws NullPointerException if the given formatter is null. + */ + @Override + public synchronized void setFormatter(Formatter newFormatter) throws SecurityException { + checkAccess(); + if (newFormatter == null) { + throw new NullPointerException(); + } + this.formatter = newFormatter; + } + + /** + * Gets the push level. The default is Level.OFF meaning that + * this Handler will only push when the internal buffer is full. + * @return the push level. + */ + public final synchronized Level getPushLevel() { + return this.pushLevel; + } + + /** + * Sets the push level. This level is used to trigger a push so that + * all pending records are formatted and sent to the email server. When + * the push level triggers a send, the resulting email is flagged as + * high importance with urgent priority. + * @param level Level object. + * @throws NullPointerException if level is null. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + public final synchronized void setPushLevel(final Level level) { + checkAccess(); + if (level == null) { + throw new NullPointerException(); + } + + if (isWriting) { + throw new IllegalStateException(); + } + this.pushLevel = level; + } + + /** + * Gets the push filter. The default is null. + * @return the push filter or null. + */ + public final synchronized Filter getPushFilter() { + return this.pushFilter; + } + + /** + * Sets the push filter. This filter is only called if the given + * LogRecord level was greater than the push level. If this + * filter returns true, all pending records are formatted and + * sent to the email server. When the push filter triggers a send, the + * resulting email is flagged as high importance with urgent priority. + * @param filter push filter or null + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + public final synchronized void setPushFilter(final Filter filter) { + checkAccess(); + if (isWriting) { + throw new IllegalStateException(); + } + this.pushFilter = filter; + } + + /** + * Gets the comparator used to order all LogRecord objects + * prior to formatting. If null then the order is unspecified. + * @return the LogRecord comparator. + */ + public final synchronized Comparator getComparator() { + return this.comparator; + } + + /** + * Sets the comparator used to order all LogRecord objects + * prior to formatting. If null then the order is unspecified. + * @param c the LogRecord comparator. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + public final synchronized void setComparator(Comparator c) { + checkAccess(); + if (isWriting) { + throw new IllegalStateException(); + } + this.comparator = c; + } + + /** + * Gets the number of log records the internal buffer can hold. When + * capacity is reached, Handler will format all + * LogRecord objects into one email message. + * @return the capacity. + */ + public final synchronized int getCapacity() { + assert capacity != Integer.MIN_VALUE && capacity != 0 : capacity; + return Math.abs(capacity); + } + + /** + * Gets the Authenticator used to login to the email server. + * @return an Authenticator or null if none is + * required. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + public final synchronized Authenticator getAuthenticator() { + checkAccess(); + return this.auth; + } + + /** + * Sets the Authenticator used to login to the email server. + * @param auth an Authenticator object or null if none is + * required. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + public final void setAuthenticator(final Authenticator auth) { + this.setAuthenticator0(auth); + } + + /** + * Sets the Authenticator used to login to the email server. + * @param password a password, empty array can be used to only supply a + * user name set by mail.user property, or null if no + * credentials are required. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + * @see String#toCharArray() + * @since JavaMail 1.4.6 + */ + public final void setAuthenticator(final char... password) { + if (password == null) { + setAuthenticator0((Authenticator) null); + } else { + setAuthenticator0(DefaultAuthenticator.of(new String(password))); + } + } + + /** + * A private hook to handle possible future overrides. See public method. + * @param auth see public method. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + private void setAuthenticator0(final Authenticator auth) { + checkAccess(); + + Session settings; + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + this.auth = auth; + settings = updateSession(); + } + verifySettings(settings); + } + + /** + * Sets the mail properties used for the session. The key/value pairs + * are defined in the Java Mail API documentation. This + * Handler will also search the LogManager for + * defaults if needed. + * @param props a non null properties object. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if props is null. + * @throws IllegalStateException if called from inside a push. + */ + public final void setMailProperties(Properties props) { + this.setMailProperties0(props); + } + + /** + * A private hook to handle overrides when the public method is declared + * non final. See public method for details. + * @param props see public method. + */ + private void setMailProperties0(Properties props) { + checkAccess(); + props = (Properties) props.clone(); //Allow subclass. + Session settings; + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + this.mailProps = props; + settings = updateSession(); + } + verifySettings(settings); + } + + /** + * Gets a copy of the mail properties used for the session. + * @return a non null properties object. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + public final Properties getMailProperties() { + checkAccess(); + final Properties props; + synchronized (this) { + props = this.mailProps; + } + return (Properties) props.clone(); + } + + /** + * Gets the attachment filters. If the attachment filter does not + * allow any LogRecord to be formatted, the attachment may + * be omitted from the email. + * @return a non null array of attachment filters. + */ + public final Filter[] getAttachmentFilters() { + return readOnlyAttachmentFilters().clone(); + } + + /** + * Sets the attachment filters. + * @param filters a non null array of filters. A + * null index value is allowed. A null value + * means that all records are allowed for the attachment at that index. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if filters is null + * @throws IndexOutOfBoundsException if the number of attachment + * name formatters do not match the number of attachment formatters. + * @throws IllegalStateException if called from inside a push. + */ + public final void setAttachmentFilters(Filter... filters) { + checkAccess(); + if (filters.length == 0) { + filters = emptyFilterArray(); + } else { + filters = Arrays.copyOf(filters, filters.length, Filter[].class); + } + synchronized (this) { + if (this.attachmentFormatters.length != filters.length) { + throw attachmentMismatch(this.attachmentFormatters.length, filters.length); + } + + if (isWriting) { + throw new IllegalStateException(); + } + + if (size != 0) { + for (int i = 0; i < filters.length; ++i) { + if (filters[i] != attachmentFilters[i]) { + clearMatches(i); + break; + } + } + } + this.attachmentFilters = filters; + } + } + + /** + * Gets the attachment formatters. This Handler is using + * attachments only if the returned array length is non zero. + * @return a non null array of formatters. + */ + public final Formatter[] getAttachmentFormatters() { + Formatter[] formatters; + synchronized (this) { + formatters = this.attachmentFormatters; + } + return formatters.clone(); + } + + /** + * Sets the attachment Formatter object for this handler. + * The number of formatters determines the number of attachments per + * email. This method should be the first attachment method called. + * To remove all attachments, call this method with empty array. + * @param formatters a non null array of formatters. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if the given array or any array index is + * null. + * @throws IllegalStateException if called from inside a push. + */ + public final void setAttachmentFormatters(Formatter... formatters) { + checkAccess(); + if (formatters.length == 0) { //Null check and length check. + formatters = emptyFormatterArray(); + } else { + formatters = Arrays.copyOf(formatters, + formatters.length, Formatter[].class); + for (int i = 0; i < formatters.length; ++i) { + if (formatters[i] == null) { + throw new NullPointerException(atIndexMsg(i)); + } + } + } + + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + + this.attachmentFormatters = formatters; + this.alignAttachmentFilters(); + this.alignAttachmentNames(); + } + } + + /** + * Gets the attachment name formatters. + * If the attachment names were set using explicit names then + * the names can be returned by calling toString on each + * attachment name formatter. + * @return non null array of attachment name formatters. + */ + public final Formatter[] getAttachmentNames() { + final Formatter[] formatters; + synchronized (this) { + formatters = this.attachmentNames; + } + return formatters.clone(); + } + + /** + * Sets the attachment file name for each attachment. All control + * characters are removed from the attachment names. + * This method will create a set of custom formatters. + * @param names an array of names. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IndexOutOfBoundsException if the number of attachment + * names do not match the number of attachment formatters. + * @throws IllegalArgumentException if any name is empty. + * @throws NullPointerException if any given array or name is + * null. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setAttachmentNames(final String... names) { + checkAccess(); + + final Formatter[] formatters; + if (names.length == 0) { + formatters = emptyFormatterArray(); + } else { + formatters = new Formatter[names.length]; + } + + for (int i = 0; i < names.length; ++i) { + final String name = names[i]; + if (name != null) { + if (name.length() > 0) { + formatters[i] = TailNameFormatter.of(name); + } else { + throw new IllegalArgumentException(atIndexMsg(i)); + } + } else { + throw new NullPointerException(atIndexMsg(i)); + } + } + + synchronized (this) { + if (this.attachmentFormatters.length != names.length) { + throw attachmentMismatch(this.attachmentFormatters.length, names.length); + } + + if (isWriting) { + throw new IllegalStateException(); + } + this.attachmentNames = formatters; + } + } + + /** + * Sets the attachment file name formatters. The format method of each + * attachment formatter will see only the LogRecord objects + * that passed its attachment filter during formatting. The format method + * will typically return an empty string. Instead of being used to format + * records, it is used to gather information about the contents of an + * attachment. The getTail method should be used to construct + * the attachment file name and reset any formatter collected state. All + * control characters will be removed from the output of the formatter. The + * toString method of the given formatter should be overridden + * to provide a useful attachment file name, if possible. + * @param formatters and array of attachment name formatters. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IndexOutOfBoundsException if the number of attachment + * name formatters do not match the number of attachment formatters. + * @throws NullPointerException if any given array or name is + * null. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setAttachmentNames(Formatter... formatters) { + checkAccess(); + + if (formatters.length == 0) { + formatters = emptyFormatterArray(); + } else { + formatters = Arrays.copyOf(formatters, formatters.length, + Formatter[].class); + } + + for (int i = 0; i < formatters.length; ++i) { + if (formatters[i] == null) { + throw new NullPointerException(atIndexMsg(i)); + } + } + + synchronized (this) { + if (this.attachmentFormatters.length != formatters.length) { + throw attachmentMismatch(this.attachmentFormatters.length, + formatters.length); + } + + if (isWriting) { + throw new IllegalStateException(); + } + + this.attachmentNames = formatters; + } + } + + /** + * Gets the formatter used to create the subject line. + * If the subject was created using a literal string then + * the toString method can be used to get the subject line. + * @return the formatter. + */ + public final synchronized Formatter getSubject() { + return this.subjectFormatter; + } + + /** + * Sets a literal string for the email subject. All control characters are + * removed from the subject line. + * @param subject a non null string. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if subject is + * null. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setSubject(final String subject) { + if (subject != null) { + this.setSubject(TailNameFormatter.of(subject)); + } else { + checkAccess(); + throw new NullPointerException(); + } + } + + /** + * Sets the subject formatter for email. The format method of the subject + * formatter will see all LogRecord objects that were published + * to this Handler during formatting and will typically return + * an empty string. This formatter is used to gather information to create + * a summary about what information is contained in the email. The + * getTail method should be used to construct the subject and + * reset any formatter collected state. All control characters + * will be removed from the formatter output. The toString + * method of the given formatter should be overridden to provide a useful + * subject, if possible. + * @param format the subject formatter. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws NullPointerException if format is null. + * @throws IllegalStateException if called from inside a push. + * @see Character#isISOControl(char) + * @see Character#isISOControl(int) + */ + public final void setSubject(final Formatter format) { + checkAccess(); + if (format == null) { + throw new NullPointerException(); + } + + synchronized (this) { + if (isWriting) { + throw new IllegalStateException(); + } + this.subjectFormatter = format; + } + } + + /** + * Protected convenience method to report an error to this Handler's + * ErrorManager. This method will prefix all non null error messages with + * Level.SEVERE.getName(). This allows the receiving error + * manager to determine if the msg parameter is a simple error + * message or a raw email message. + * @param msg a descriptive string (may be null) + * @param ex an exception (may be null) + * @param code an error code defined in ErrorManager + */ + @Override + protected void reportError(String msg, Exception ex, int code) { + try { + if (msg != null) { + errorManager.error(Level.SEVERE.getName() + .concat(": ").concat(msg), ex, code); + } else { + errorManager.error(null, ex, code); + } + } catch (RuntimeException | LinkageError GLASSFISH_21258) { + reportLinkageError(GLASSFISH_21258, code); + } + } + + /** + * Calls log manager checkAccess if this is sealed. + */ + private void checkAccess() { + if (sealed) { + LogManagerProperties.checkLogManagerAccess(); + } + } + + /** + * Determines the mimeType of a formatter from the getHead call. + * This could be made protected, or a new class could be created to do + * this type of conversion. Currently, this is only used for the body + * since the attachments are computed by filename. + * Package-private for unit testing. + * @param chunk any char sequence or null. + * @return return the mime type or null for text/plain. + */ + final String contentTypeOf(CharSequence chunk) { + if (!isEmpty(chunk)) { + final int MAX_CHARS = 25; + if (chunk.length() > MAX_CHARS) { + chunk = chunk.subSequence(0, MAX_CHARS); + } + try { + final String charset = getEncodingName(); + final byte[] b = chunk.toString().getBytes(charset); + final ByteArrayInputStream in = new ByteArrayInputStream(b); + assert in.markSupported() : in.getClass().getName(); + return URLConnection.guessContentTypeFromStream(in); + } catch (final IOException IOE) { + reportError(IOE.getMessage(), IOE, ErrorManager.FORMAT_FAILURE); + } + } + return null; //text/plain + } + + /** + * Determines the mimeType of a formatter by the class name. This method + * avoids calling getHead and getTail of content formatters during verify + * because they might trigger side effects or excessive work. The name + * formatters and subject are usually safe to call. + * Package-private for unit testing. + * + * @param f the formatter or null. + * @return return the mime type or null, meaning text/plain. + * @since JavaMail 1.5.6 + */ + final String contentTypeOf(final Formatter f) { + assert Thread.holdsLock(this); + if (f != null) { + String type = getContentType(f.getClass().getName()); + if (type != null) { + return type; + } + + for (Class k = f.getClass(); k != Formatter.class; + k = k.getSuperclass()) { + String name; + try { + name = k.getSimpleName(); + } catch (final InternalError JDK8057919) { + name = k.getName(); + } + name = name.toLowerCase(Locale.ENGLISH); + for (int idx = name.indexOf('$') + 1; + (idx = name.indexOf("ml", idx)) > -1; idx += 2) { + if (idx > 0) { + if (name.charAt(idx - 1) == 'x') { + return "application/xml"; + } + if (idx > 1 && name.charAt(idx - 2) == 'h' + && name.charAt(idx - 1) == 't') { + return "text/html"; + } + } + } + } + } + return null; + } + + /** + * Determines if the given throwable is a no content exception. It is + * assumed Transport.sendMessage will call Message.writeTo so we need to + * ignore any exceptions that could be layered on top of that call chain to + * infer that sendMessage is failing because of writeTo. Package-private + * for unit testing. + * @param msg the message without content. + * @param t the throwable chain to test. + * @return true if the throwable is a missing content exception. + * @throws NullPointerException if any of the arguments are null. + * @since JavaMail 1.4.5 + */ + @SuppressWarnings({"UseSpecificCatch", "ThrowableResultIgnored"}) + final boolean isMissingContent(Message msg, Throwable t) { + final Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + msg.writeTo(new ByteArrayOutputStream(MIN_HEADER_SIZE)); + } catch (final RuntimeException RE) { + throw RE; //Avoid catch all. + } catch (final Exception noContent) { + final String txt = noContent.getMessage(); + if (!isEmpty(txt)) { + int limit = 0; + while (t != null) { + if (noContent.getClass() == t.getClass() + && txt.equals(t.getMessage())) { + return true; + } + + //Not all Jakarta Mail implementations support JDK 1.4 + //exception chaining. + final Throwable cause = t.getCause(); + if (cause == null && t instanceof MessagingException) { + t = ((MessagingException) t).getNextException(); + } else { + t = cause; + } + + //Deal with excessive cause chains and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + } + } finally { + getAndSetContextClassLoader(ccl); + } + return false; + } + + /** + * Converts a mime message to a raw string or formats the reason + * why message can't be changed to raw string and reports it. + * @param msg the mime message. + * @param ex the original exception. + * @param code the ErrorManager code. + * @since JavaMail 1.4.5 + */ + @SuppressWarnings("UseSpecificCatch") + private void reportError(Message msg, Exception ex, int code) { + try { + try { //Use direct call so we do not prefix raw email. + errorManager.error(toRawString(msg), ex, code); + } catch (final RuntimeException re) { + reportError(toMsgString(re), ex, code); + } catch (final Exception e) { + reportError(toMsgString(e), ex, code); + } + } catch (final LinkageError GLASSFISH_21258) { + reportLinkageError(GLASSFISH_21258, code); + } + } + + /** + * Reports the given linkage error or runtime exception. + * + * The current LogManager code will stop closing all remaining handlers if + * an error is thrown during resetLogger. This is a workaround for + * GLASSFISH-21258 and JDK-8152515. + * @param le the linkage error or a RuntimeException. + * @param code the ErrorManager code. + * @throws NullPointerException if error is null. + * @since JavaMail 1.5.3 + */ + private void reportLinkageError(final Throwable le, final int code) { + if (le == null) { + throw new NullPointerException(String.valueOf(code)); + } + + final Integer idx = MUTEX.get(); + if (idx == null || idx > MUTEX_LINKAGE) { + MUTEX.set(MUTEX_LINKAGE); + try { + Thread.currentThread().getUncaughtExceptionHandler() + .uncaughtException(Thread.currentThread(), le); + } catch (RuntimeException | LinkageError ignore) { + } finally { + if (idx != null) { + MUTEX.set(idx); + } else { + MUTEX.remove(); + } + } + } + } + + /** + * Determines the mimeType from the given file name. + * Used to override the body content type and used for all attachments. + * @param name the file name or class name. + * @return the mime type or null for text/plain. + */ + private String getContentType(final String name) { + assert Thread.holdsLock(this); + final String type = contentTypes.getContentType(name); + if ("application/octet-stream".equalsIgnoreCase(type)) { + return null; //Formatters return strings, default to text/plain. + } + return type; + } + + /** + * Gets the encoding set for this handler, mime encoding, or file encoding. + * @return the java charset name, never null. + * @since JavaMail 1.4.5 + */ + private String getEncodingName() { + String charset = getEncoding(); + if (charset == null) { + charset = MimeUtility.getDefaultJavaCharset(); + } + return charset; + } + + /** + * Set the content for a part using the encoding assigned to the handler. + * @param part the part to assign. + * @param buf the formatted data. + * @param type the mime type or null, meaning text/plain. + * @throws MessagingException if there is a problem. + */ + private void setContent(MimePart part, CharSequence buf, String type) throws MessagingException { + final String charset = getEncodingName(); + if (type != null && !"text/plain".equalsIgnoreCase(type)) { + type = contentWithEncoding(type, charset); + try { + DataSource source = new ByteArrayDataSource(buf.toString(), type); + part.setDataHandler(new DataHandler(source)); + } catch (final IOException IOE) { + reportError(IOE.getMessage(), IOE, ErrorManager.FORMAT_FAILURE); + part.setText(buf.toString(), charset); + } + } else { + part.setText(buf.toString(), MimeUtility.mimeCharset(charset)); + } + } + + /** + * Replaces the charset parameter with the current encoding. + * @param type the content type. + * @param encoding the java charset name. + * @return the type with a specified encoding. + */ + private String contentWithEncoding(String type, String encoding) { + assert encoding != null; + try { + final ContentType ct = new ContentType(type); + ct.setParameter("charset", MimeUtility.mimeCharset(encoding)); + encoding = ct.toString(); //See javax.mail.internet.ContentType. + if (!isEmpty(encoding)) { //Support pre K5687. + type = encoding; + } + } catch (final MessagingException ME) { + reportError(type, ME, ErrorManager.FORMAT_FAILURE); + } + return type; + } + + /** + * Sets the capacity for this handler. This method is kept private + * because we would have to define a public policy for when the size is + * greater than the capacity. + * E.G. do nothing, flush now, truncate now, push now and resize. + * @param newCapacity the max number of records. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + * @throws IllegalStateException if called from inside a push. + */ + private synchronized void setCapacity0(final int newCapacity) { + checkAccess(); + if (newCapacity <= 0) { + throw new IllegalArgumentException("Capacity must be greater than zero."); + } + + if (isWriting) { + throw new IllegalStateException(); + } + + if (this.capacity < 0) { //If closed, remain closed. + this.capacity = -newCapacity; + } else { + this.capacity = newCapacity; + } + } + + /** + * Gets the attachment filters using a happens-before relationship between + * this method and setAttachmentFilters. The attachment filters are treated + * as copy-on-write, so the returned array must never be modified or + * published outside this class. + * @return a read only array of filters. + */ + private Filter[] readOnlyAttachmentFilters() { + return this.attachmentFilters; + } + + /** + * Factory for empty formatter arrays. + * @return an empty array. + */ + private static Formatter[] emptyFormatterArray() { + return EMPTY_FORMATTERS; + } + + /** + * Factory for empty filter arrays. + * @return an empty array. + */ + private static Filter[] emptyFilterArray() { + return EMPTY_FILTERS; + } + + /** + * Expand or shrink the attachment name formatters with the attachment + * formatters. + * @return true if size was changed. + */ + private boolean alignAttachmentNames() { + assert Thread.holdsLock(this); + boolean fixed = false; + final int expect = this.attachmentFormatters.length; + final int current = this.attachmentNames.length; + if (current != expect) { + this.attachmentNames = Arrays.copyOf(attachmentNames, expect, + Formatter[].class); + fixed = current != 0; + } + + //Copy of zero length array is cheap, warm up copyOf. + if (expect == 0) { + this.attachmentNames = emptyFormatterArray(); + assert this.attachmentNames.length == 0; + } else { + for (int i = 0; i < expect; ++i) { + if (this.attachmentNames[i] == null) { + this.attachmentNames[i] = TailNameFormatter.of( + toString(this.attachmentFormatters[i])); + } + } + } + return fixed; + } + + /** + * Expand or shrink the attachment filters with the attachment formatters. + * @return true if the size was changed. + */ + private boolean alignAttachmentFilters() { + assert Thread.holdsLock(this); + + boolean fixed = false; + final int expect = this.attachmentFormatters.length; + final int current = this.attachmentFilters.length; + if (current != expect) { + this.attachmentFilters = Arrays.copyOf(attachmentFilters, expect, + Filter[].class); + clearMatches(current); + fixed = current != 0; + + //Array elements default to null so skip filling if body filter + //is null. If not null then only assign to expanded elements. + final Filter body = this.filter; + if (body != null) { + for (int i = current; i < expect; ++i) { + this.attachmentFilters[i] = body; + } + } + } + + //Copy of zero length array is cheap, warm up copyOf. + if (expect == 0) { + this.attachmentFilters = emptyFilterArray(); + assert this.attachmentFilters.length == 0; + } + return fixed; + } + + /** + * Sets the size to zero and clears the current buffer. + */ + private void reset() { + assert Thread.holdsLock(this); + if (size < data.length) { + Arrays.fill(data, 0, size, null); + } else { + Arrays.fill(data, null); + } + this.size = 0; + } + + /** + * Expands the internal buffer up to the capacity. + */ + private void grow() { + assert Thread.holdsLock(this); + final int len = data.length; + int newCapacity = len + (len >> 1) + 1; + if (newCapacity > capacity || newCapacity < len) { + newCapacity = capacity; + } + assert len != capacity : len; + this.data = Arrays.copyOf(data, newCapacity, LogRecord[].class); + this.matched = Arrays.copyOf(matched, newCapacity); + } + + /** + * Configures the handler properties from the log manager. + * @param props the given mail properties. Maybe null and are never + * captured by this handler. + * @throws SecurityException if a security manager exists and the + * caller does not have LoggingPermission("control"). + */ + private synchronized void init(final Properties props) { + assert this.errorManager != null; + final String p = getClass().getName(); + this.mailProps = new Properties(); //See method param comments. + final Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + this.contentTypes = FileTypeMap.getDefaultFileTypeMap(); + } finally { + getAndSetContextClassLoader(ccl); + } + + //Assign any custom error manager first so it can detect all failures. + initErrorManager(p); + + initLevel(p); + initFilter(p); + initCapacity(p); + initAuthenticator(p); + + initEncoding(p); + initFormatter(p); + initComparator(p); + initPushLevel(p); + initPushFilter(p); + + initSubject(p); + + initAttachmentFormaters(p); + initAttachmentFilters(p); + initAttachmentNames(p); + + if (props == null && fromLogManager(p.concat(".verify")) != null) { + verifySettings(initSession()); + } + intern(); //Show verify warnings first. + } + + /** + * Interns the error manager, formatters, and filters contained in this + * handler. The comparator is not interned. This method can only be + * called from init after all of formatters and filters are in a constructed + * and in a consistent state. + * @since JavaMail 1.5.0 + */ + private void intern() { + assert Thread.holdsLock(this); + try { + Object canidate; + Object result; + final Map seen = new HashMap<>(); + try { + intern(seen, this.errorManager); + } catch (final SecurityException se) { + reportError(se.getMessage(), se, ErrorManager.OPEN_FAILURE); + } + + try { + canidate = this.filter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Filter) { + this.filter = (Filter) result; + } + + canidate = this.formatter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + this.formatter = (Formatter) result; + } + } catch (final SecurityException se) { + reportError(se.getMessage(), se, ErrorManager.OPEN_FAILURE); + } + + canidate = this.subjectFormatter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + this.subjectFormatter = (Formatter) result; + } + + canidate = this.pushFilter; + result = intern(seen, canidate); + if (result != canidate && result instanceof Filter) { + this.pushFilter = (Filter) result; + } + + for (int i = 0; i < attachmentFormatters.length; ++i) { + canidate = attachmentFormatters[i]; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + attachmentFormatters[i] = (Formatter) result; + } + + canidate = attachmentFilters[i]; + result = intern(seen, canidate); + if (result != canidate && result instanceof Filter) { + attachmentFilters[i] = (Filter) result; + } + + canidate = attachmentNames[i]; + result = intern(seen, canidate); + if (result != canidate && result instanceof Formatter) { + attachmentNames[i] = (Formatter) result; + } + } + } catch (final Exception skip) { + reportError(skip.getMessage(), skip, ErrorManager.OPEN_FAILURE); + } catch (final LinkageError skip) { + reportError(skip.getMessage(), new InvocationTargetException(skip), + ErrorManager.OPEN_FAILURE); + } + } + + /** + * If possible performs an intern of the given object into the + * map. If the object can not be interned the given object is returned. + * @param m the map used to record the interned values. + * @param o the object to try an intern. + * @return the original object or an intern replacement. + * @throws SecurityException if this operation is not allowed by the + * security manager. + * @throws Exception if there is an unexpected problem. + * @since JavaMail 1.5.0 + */ + private Object intern(Map m, Object o) throws Exception { + if (o == null) { + return null; + } + + /** + * The common case is that most objects will not intern. The given + * object has a public no argument constructor or is an instance of a + * TailNameFormatter. TailNameFormatter is safe use as a map key. + * For everything else we create a clone of the given object. + * This is done because of the following: + * 1. Clones can be used to test that a class provides an equals method + * and that the equals method works correctly. + * 2. Calling equals on the given object is assumed to be cheap. + * 3. The intern map can be filtered so it only contains objects that + * can be interned, which reduces the memory footprint. + * 4. Clones are method local garbage. + * 5. Hash code is only called on the clones so bias locking is not + * disabled on the objects the handler will use. + */ + final Object key; + if (o.getClass().getName().equals(TailNameFormatter.class.getName())) { + key = o; + } else { + //This call was already made in the LogManagerProperties so this + //shouldn't trigger loading of any lazy reflection code. + key = o.getClass().getConstructor().newInstance(); + } + + final Object use; + //Check the classloaders of each object avoiding the security manager. + if (key.getClass() == o.getClass()) { + Object found = m.get(key); //Transitive equals test. + if (found == null) { + //Ensure that equals is symmetric to prove intern is safe. + final boolean right = key.equals(o); + final boolean left = o.equals(key); + if (right && left) { + //Assume hashCode is defined at this point. + found = m.put(o, o); + if (found != null) { + reportNonDiscriminating(key, found); + found = m.remove(key); + if (found != o) { + reportNonDiscriminating(key, found); + m.clear(); //Try to restore order. + } + } + } else { + if (right != left) { + reportNonSymmetric(o, key); + } + } + use = o; + } else { + //Check for a discriminating equals method. + if (o.getClass() == found.getClass()) { + use = found; + } else { + reportNonDiscriminating(o, found); + use = o; + } + } + } else { + use = o; + } + return use; + } + + /** + * Factory method used to create a java.util.logging.SimpleFormatter. + * @return a new SimpleFormatter. + * @since JavaMail 1.5.6 + */ + private static Formatter createSimpleFormatter() { + //Don't force the byte code verifier to load the formatter. + return Formatter.class.cast(new SimpleFormatter()); + } + + /** + * Checks a char sequence value for null or empty. + * @param s the char sequence. + * @return true if the given string is null or zero length. + */ + private static boolean isEmpty(final CharSequence s) { + return s == null || s.length() == 0; + } + + /** + * Checks that a string is not empty and not equal to the literal "null". + * @param name the string to check for a value. + * @return true if the string has a valid value. + */ + private static boolean hasValue(final String name) { + return !isEmpty(name) && !"null".equalsIgnoreCase(name); + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAttachmentFilters(final String p) { + assert Thread.holdsLock(this); + assert this.attachmentFormatters != null; + final String list = fromLogManager(p.concat(".attachment.filters")); + if (!isEmpty(list)) { + final String[] names = list.split(","); + Filter[] a = new Filter[names.length]; + for (int i = 0; i < a.length; ++i) { + names[i] = names[i].trim(); + if (!"null".equalsIgnoreCase(names[i])) { + try { + a[i] = LogManagerProperties.newFilter(names[i]); + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + } + + this.attachmentFilters = a; + if (alignAttachmentFilters()) { + reportError("Attachment filters.", + attachmentMismatch("Length mismatch."), ErrorManager.OPEN_FAILURE); + } + } else { + this.attachmentFilters = emptyFilterArray(); + alignAttachmentFilters(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAttachmentFormaters(final String p) { + assert Thread.holdsLock(this); + final String list = fromLogManager(p.concat(".attachment.formatters")); + if (!isEmpty(list)) { + final Formatter[] a; + final String[] names = list.split(","); + if (names.length == 0) { + a = emptyFormatterArray(); + } else { + a = new Formatter[names.length]; + } + + for (int i = 0; i < a.length; ++i) { + names[i] = names[i].trim(); + if (!"null".equalsIgnoreCase(names[i])) { + try { + a[i] = LogManagerProperties.newFormatter(names[i]); + if (a[i] instanceof TailNameFormatter) { + final Exception CNFE = new ClassNotFoundException(a[i].toString()); + reportError("Attachment formatter.", CNFE, ErrorManager.OPEN_FAILURE); + a[i] = createSimpleFormatter(); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + a[i] = createSimpleFormatter(); + } + } else { + final Exception NPE = new NullPointerException(atIndexMsg(i)); + reportError("Attachment formatter.", NPE, ErrorManager.OPEN_FAILURE); + a[i] = createSimpleFormatter(); + } + } + + this.attachmentFormatters = a; + } else { + this.attachmentFormatters = emptyFormatterArray(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAttachmentNames(final String p) { + assert Thread.holdsLock(this); + assert this.attachmentFormatters != null; + + final String list = fromLogManager(p.concat(".attachment.names")); + if (!isEmpty(list)) { + final String[] names = list.split(","); + final Formatter[] a = new Formatter[names.length]; + for (int i = 0; i < a.length; ++i) { + names[i] = names[i].trim(); + if (!"null".equalsIgnoreCase(names[i])) { + try { + try { + a[i] = LogManagerProperties.newFormatter(names[i]); + } catch (ClassNotFoundException + | ClassCastException literal) { + a[i] = TailNameFormatter.of(names[i]); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } else { + final Exception NPE = new NullPointerException(atIndexMsg(i)); + reportError("Attachment names.", NPE, ErrorManager.OPEN_FAILURE); + } + } + + this.attachmentNames = a; + if (alignAttachmentNames()) { //Any null indexes are repaired. + reportError("Attachment names.", + attachmentMismatch("Length mismatch."), ErrorManager.OPEN_FAILURE); + } + } else { + this.attachmentNames = emptyFormatterArray(); + alignAttachmentNames(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initAuthenticator(final String p) { + assert Thread.holdsLock(this); + String name = fromLogManager(p.concat(".authenticator")); + if (name != null && !"null".equalsIgnoreCase(name)) { + if (name.length() != 0) { + try { + this.auth = LogManagerProperties + .newObjectFrom(name, Authenticator.class); + } catch (final SecurityException SE) { + throw SE; + } catch (final ClassNotFoundException + | ClassCastException literalAuth) { + this.auth = DefaultAuthenticator.of(name); + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } else { //Authenticator is installed to provide the user name. + this.auth = DefaultAuthenticator.of(name); + } + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initLevel(final String p) { + assert Thread.holdsLock(this); + try { + final String val = fromLogManager(p.concat(".level")); + if (val != null) { + logLevel = Level.parse(val); + } else { + logLevel = Level.WARNING; + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); + logLevel = Level.WARNING; + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initFilter(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".filter")); + if (hasValue(name)) { + filter = LogManagerProperties.newFilter(name); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if argument is null. + * @throws SecurityException if not allowed. + */ + private void initCapacity(final String p) { + assert Thread.holdsLock(this); + final int DEFAULT_CAPACITY = 1000; + try { + final String value = fromLogManager(p.concat(".capacity")); + if (value != null) { + this.setCapacity0(Integer.parseInt(value)); + } else { + this.setCapacity0(DEFAULT_CAPACITY); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); + } + + if (capacity <= 0) { + capacity = DEFAULT_CAPACITY; + } + + this.data = new LogRecord[1]; + this.matched = new int[this.data.length]; + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initEncoding(final String p) { + assert Thread.holdsLock(this); + try { + String e = fromLogManager(p.concat(".encoding")); + if (e != null) { + setEncoding0(e); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (UnsupportedEncodingException | RuntimeException UEE) { + reportError(UEE.getMessage(), UEE, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Used to get or create the default ErrorManager used before init. + * @return the super error manager or a new ErrorManager. + * @since JavaMail 1.5.3 + */ + private ErrorManager defaultErrorManager() { + ErrorManager em; + try { //Try to share the super error manager. + em = super.getErrorManager(); + } catch (RuntimeException | LinkageError ignore) { + em = null; + } + + //Don't assume that the super call is not null. + if (em == null) { + em = new ErrorManager(); + } + return em; + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initErrorManager(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".errorManager")); + if (name != null) { + setErrorManager0(LogManagerProperties.newErrorManager(name)); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initFormatter(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".formatter")); + if (hasValue(name)) { + final Formatter f + = LogManagerProperties.newFormatter(name); + assert f != null; + if (f instanceof TailNameFormatter == false) { + formatter = f; + } else { + formatter = createSimpleFormatter(); + } + } else { + formatter = createSimpleFormatter(); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + formatter = createSimpleFormatter(); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initComparator(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".comparator")); + String reverse = fromLogManager(p.concat(".comparator.reverse")); + if (hasValue(name)) { + comparator = LogManagerProperties.newComparator(name); + if (Boolean.parseBoolean(reverse)) { + assert comparator != null : "null"; + comparator = LogManagerProperties.reverseOrder(comparator); + } + } else { + if (!isEmpty(reverse)) { + throw new IllegalArgumentException( + "No comparator to reverse."); + } + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initPushLevel(final String p) { + assert Thread.holdsLock(this); + try { + final String val = fromLogManager(p.concat(".pushLevel")); + if (val != null) { + this.pushLevel = Level.parse(val); + } + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); + } + + if (this.pushLevel == null) { + this.pushLevel = Level.OFF; + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initPushFilter(final String p) { + assert Thread.holdsLock(this); + try { + String name = fromLogManager(p.concat(".pushFilter")); + if (hasValue(name)) { + this.pushFilter = LogManagerProperties.newFilter(name); + } + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (final Exception E) { + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Parses LogManager string values into objects used by this handler. + * @param p the handler class name used as the prefix. + * @throws NullPointerException if the given argument is null. + * @throws SecurityException if not allowed. + */ + private void initSubject(final String p) { + assert Thread.holdsLock(this); + String name = fromLogManager(p.concat(".subject")); + if (name == null) { //Soft dependency on CollectorFormatter. + name = "com.sun.mail.util.logging.CollectorFormatter"; + } + + if (hasValue(name)) { + try { + this.subjectFormatter = LogManagerProperties.newFormatter(name); + } catch (final SecurityException SE) { + throw SE; //Avoid catch all. + } catch (ClassNotFoundException + | ClassCastException literalSubject) { + this.subjectFormatter = TailNameFormatter.of(name); + } catch (final Exception E) { + this.subjectFormatter = TailNameFormatter.of(name); + reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); + } + } else { //User has forced empty or literal null. + this.subjectFormatter = TailNameFormatter.of(name); + } + } + + /** + * Check if any attachment would actually format the given + * LogRecord. This method does not check if the handler + * is level is set to OFF or if the handler is closed. + * @param record a LogRecord + * @return true if the LogRecord would be formatted. + */ + private boolean isAttachmentLoggable(final LogRecord record) { + final Filter[] filters = readOnlyAttachmentFilters(); + for (int i = 0; i < filters.length; ++i) { + final Filter f = filters[i]; + if (f == null || f.isLoggable(record)) { + setMatchedPart(i); + return true; + } + } + return false; + } + + /** + * Check if this Handler would push after storing the + * LogRecord into its internal buffer. + * @param record a LogRecord + * @return true if the LogRecord triggers an email push. + * @throws NullPointerException if tryMutex was not called. + */ + private boolean isPushable(final LogRecord record) { + assert Thread.holdsLock(this); + final int value = getPushLevel().intValue(); + if (value == offValue || record.getLevel().intValue() < value) { + return false; + } + + final Filter push = getPushFilter(); + if (push == null) { + return true; + } + + final int match = getMatchedPart(); + if ((match == -1 && getFilter() == push) + || (match >= 0 && attachmentFilters[match] == push)) { + return true; + } else { + return push.isLoggable(record); + } + } + + /** + * Used to perform push or flush. + * @param priority true for high priority otherwise false for normal. + * @param code the error manager code. + */ + private void push(final boolean priority, final int code) { + if (tryMutex()) { + try { + final Message msg = writeLogRecords(code); + if (msg != null) { + send(msg, priority, code); + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, code); + } finally { + releaseMutex(); + } + } else { + reportUnPublishedError(null); + } + } + + /** + * Used to send the generated email or write its contents to the + * error manager for this handler. This method does not hold any + * locks so new records can be added to this handler during a send or + * failure. + * @param msg the message or null. + * @param priority true for high priority or false for normal. + * @param code the ErrorManager code. + * @throws NullPointerException if message is null. + */ + private void send(Message msg, boolean priority, int code) { + try { + envelopeFor(msg, priority); + final Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { //JDK-8025251 + Transport.send(msg); //Calls save changes. + } finally { + getAndSetContextClassLoader(ccl); + } + } catch (final RuntimeException re) { + reportError(msg, re, code); + } catch (final Exception e) { + reportError(msg, e, code); + } + } + + /** + * Performs a sort on the records if needed. + * Any exception thrown during a sort is considered a formatting error. + */ + private void sort() { + assert Thread.holdsLock(this); + if (comparator != null) { + try { + if (size != 1) { + Arrays.sort(data, 0, size, comparator); + } else { + if (comparator.compare(data[0], data[0]) != 0) { + throw new IllegalArgumentException( + comparator.getClass().getName()); + } + } + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Formats all records in the buffer and places the output in a Message. + * This method under most conditions will catch, report, and continue when + * exceptions occur. This method holds a lock on this handler. + * @param code the error manager code. + * @return null if there are no records or is currently in a push. + * Otherwise a new message is created with a formatted message and + * attached session. + */ + private Message writeLogRecords(final int code) { + try { + synchronized (this) { + if (size > 0 && !isWriting) { + isWriting = true; + try { + return writeLogRecords0(); + } finally { + isWriting = false; + if (size > 0) { + reset(); + } + } + } + } + } catch (final RuntimeException re) { + reportError(re.getMessage(), re, code); + } catch (final Exception e) { + reportError(e.getMessage(), e, code); + } + return null; + } + + /** + * Formats all records in the buffer and places the output in a Message. + * This method under most conditions will catch, report, and continue when + * exceptions occur. + * + * @return null if there are no records or is currently in a push. Otherwise + * a new message is created with a formatted message and attached session. + * @throws MessagingException if there is a problem. + * @throws IOException if there is a problem. + * @throws RuntimeException if there is an unexpected problem. + * @since JavaMail 1.5.3 + */ + private Message writeLogRecords0() throws Exception { + assert Thread.holdsLock(this); + sort(); + if (session == null) { + initSession(); + } + MimeMessage msg = new MimeMessage(session); + + /** + * Parts are lazily created when an attachment performs a getHead + * call. Therefore, a null part at an index means that the head is + * required. + */ + MimeBodyPart[] parts = new MimeBodyPart[attachmentFormatters.length]; + + /** + * The buffers are lazily created when the part requires a getHead. + */ + StringBuilder[] buffers = new StringBuilder[parts.length]; + StringBuilder buf = null; + final MimePart body; + if (parts.length == 0) { + msg.setDescription(descriptionFrom( + getFormatter(), getFilter(), subjectFormatter)); + body = msg; + } else { + msg.setDescription(descriptionFrom( + comparator, pushLevel, pushFilter)); + body = createBodyPart(); + } + + appendSubject(msg, head(subjectFormatter)); + final Formatter bodyFormat = getFormatter(); + final Filter bodyFilter = getFilter(); + + Locale lastLocale = null; + for (int ix = 0; ix < size; ++ix) { + boolean formatted = false; + final int match = matched[ix]; + final LogRecord r = data[ix]; + data[ix] = null; //Clear while formatting. + + final Locale locale = localeFor(r); + appendSubject(msg, format(subjectFormatter, r)); + Filter lmf = null; //Identity of last matched filter. + if (bodyFilter == null || match == -1 || parts.length == 0 + || (match < -1 && bodyFilter.isLoggable(r))) { + lmf = bodyFilter; + if (buf == null) { + buf = new StringBuilder(); + buf.append(head(bodyFormat)); + } + formatted = true; + buf.append(format(bodyFormat, r)); + if (locale != null && !locale.equals(lastLocale)) { + appendContentLang(body, locale); + } + } + + for (int i = 0; i < parts.length; ++i) { + //A match index less than the attachment index means that + //the filter has not seen this record. + final Filter af = attachmentFilters[i]; + if (af == null || lmf == af || match == i + || (match < i && af.isLoggable(r))) { + if (lmf == null && af != null) { + lmf = af; + } + if (parts[i] == null) { + parts[i] = createBodyPart(i); + buffers[i] = new StringBuilder(); + buffers[i].append(head(attachmentFormatters[i])); + appendFileName(parts[i], head(attachmentNames[i])); + } + formatted = true; + appendFileName(parts[i], format(attachmentNames[i], r)); + buffers[i].append(format(attachmentFormatters[i], r)); + if (locale != null && !locale.equals(lastLocale)) { + appendContentLang(parts[i], locale); + } + } + } + + if (formatted) { + if (body != msg && locale != null + && !locale.equals(lastLocale)) { + appendContentLang(msg, locale); + } + } else { //Belongs to no mime part. + reportFilterError(r); + } + lastLocale = locale; + } + this.size = 0; + + for (int i = parts.length - 1; i >= 0; --i) { + if (parts[i] != null) { + appendFileName(parts[i], tail(attachmentNames[i], "err")); + buffers[i].append(tail(attachmentFormatters[i], "")); + + if (buffers[i].length() > 0) { + String name = parts[i].getFileName(); + if (isEmpty(name)) { //Exceptional case. + name = toString(attachmentFormatters[i]); + parts[i].setFileName(name); + } + setContent(parts[i], buffers[i], getContentType(name)); + } else { + setIncompleteCopy(msg); + parts[i] = null; //Skip this part. + } + buffers[i] = null; + } + } + + if (buf != null) { + buf.append(tail(bodyFormat, "")); + //This body part is always added, even if the buffer is empty, + //so the body is never considered an incomplete-copy. + } else { + buf = new StringBuilder(0); + } + + appendSubject(msg, tail(subjectFormatter, "")); + + String contentType = contentTypeOf(buf); + String altType = contentTypeOf(bodyFormat); + setContent(body, buf, altType == null ? contentType : altType); + if (body != msg) { + final MimeMultipart multipart = new MimeMultipart(); + //assert body instanceof BodyPart : body; + multipart.addBodyPart((BodyPart) body); + + for (int i = 0; i < parts.length; ++i) { + if (parts[i] != null) { + multipart.addBodyPart(parts[i]); + } + } + msg.setContent(multipart); + } + + return msg; + } + + /** + * Checks all of the settings if the caller requests a verify and a verify + * was not performed yet and no verify is in progress. A verify is + * performed on create because this handler may be at the end of a handler + * chain and therefore may not see any log records until LogManager.reset() + * is called and at that time all of the settings have been cleared. + * @param session the current session or null. + * @since JavaMail 1.4.4 + */ + private void verifySettings(final Session session) { + try { + if (session != null) { + final Properties props = session.getProperties(); + final Object check = props.put("verify", ""); + if (check instanceof String) { + String value = (String) check; + //Perform the verify if needed. + if (hasValue(value)) { + verifySettings0(session, value); + } + } else { + if (check != null) { //Pass some invalid string. + verifySettings0(session, check.getClass().toString()); + } + } + } + } catch (final LinkageError JDK8152515) { + reportLinkageError(JDK8152515, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Checks all of the settings using the given setting. + * This triggers the LogManagerProperties to copy all of the mail + * settings without explictly knowing them. Once all of the properties + * are copied this handler can handle LogManager.reset clearing all of the + * properties. It is expected that this method is, at most, only called + * once per session. + * @param session the current session. + * @param verify the type of verify to perform. + * @since JavaMail 1.4.4 + */ + private void verifySettings0(Session session, String verify) { + assert verify != null : (String) null; + if (!"local".equals(verify) && !"remote".equals(verify) + && !"limited".equals(verify) && !"resolve".equals(verify) + && !"login".equals(verify)) { + reportError("Verify must be 'limited', local', " + + "'resolve', 'login', or 'remote'.", + new IllegalArgumentException(verify), + ErrorManager.OPEN_FAILURE); + return; + } + + final MimeMessage abort = new MimeMessage(session); + final String msg; + if (!"limited".equals(verify)) { + msg = "Local address is " + + InternetAddress.getLocalAddress(session) + '.'; + + try { //Verify subclass or declared mime charset. + Charset.forName(getEncodingName()); + } catch (final RuntimeException RE) { + UnsupportedEncodingException UEE = + new UnsupportedEncodingException(RE.toString()); + UEE.initCause(RE); + reportError(msg, UEE, ErrorManager.FORMAT_FAILURE); + } + } else { + msg = "Skipping local address check."; + } + + //Perform all of the copy actions first. + String[] atn; + synchronized (this) { //Create the subject. + appendSubject(abort, head(subjectFormatter)); + appendSubject(abort, tail(subjectFormatter, "")); + atn = new String[attachmentNames.length]; + for (int i = 0; i < atn.length; ++i) { + atn[i] = head(attachmentNames[i]); + if (atn[i].length() == 0) { + atn[i] = tail(attachmentNames[i], ""); + } else { + atn[i] = atn[i].concat(tail(attachmentNames[i], "")); + } + } + } + + setIncompleteCopy(abort); //Original body part is never added. + envelopeFor(abort, true); + saveChangesNoContent(abort, msg); + try { + //Ensure transport provider is installed. + Address[] all = abort.getAllRecipients(); + if (all == null) { //Don't pass null to sendMessage. + all = new InternetAddress[0]; + } + Transport t; + try { + final Address[] any = all.length != 0 ? all : abort.getFrom(); + if (any != null && any.length != 0) { + t = session.getTransport(any[0]); + session.getProperty("mail.transport.protocol"); //Force copy + } else { + MessagingException me = new MessagingException( + "No recipient or from address."); + reportError(msg, me, ErrorManager.OPEN_FAILURE); + throw me; + } + } catch (final MessagingException protocol) { + //Switching the CCL emulates the current send behavior. + Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + t = session.getTransport(); + } catch (final MessagingException fail) { + throw attach(protocol, fail); + } finally { + getAndSetContextClassLoader(ccl); + } + } + + String local = null; + if ("remote".equals(verify) || "login".equals(verify)) { + MessagingException closed = null; + t.connect(); + try { + try { + //Capture localhost while connection is open. + local = getLocalHost(t); + + //A message without content will fail at message writeTo + //when sendMessage is called. This allows the handler + //to capture all mail properties set in the LogManager. + if ("remote".equals(verify)) { + t.sendMessage(abort, all); + } + } finally { + try { + t.close(); + } catch (final MessagingException ME) { + closed = ME; + } + } + //Close the transport before reporting errors. + if ("remote".equals(verify)) { + reportUnexpectedSend(abort, verify, null); + } else { + final String protocol = t.getURLName().getProtocol(); + verifyProperties(session, protocol); + } + } catch (final SendFailedException sfe) { + Address[] recip = sfe.getInvalidAddresses(); + if (recip != null && recip.length != 0) { + setErrorContent(abort, verify, sfe); + reportError(abort, sfe, ErrorManager.OPEN_FAILURE); + } + + recip = sfe.getValidSentAddresses(); + if (recip != null && recip.length != 0) { + reportUnexpectedSend(abort, verify, sfe); + } + } catch (final MessagingException ME) { + if (!isMissingContent(abort, ME)) { + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + } + + if (closed != null) { + setErrorContent(abort, verify, closed); + reportError(abort, closed, ErrorManager.CLOSE_FAILURE); + } + } else { + //Force a property copy, JDK-7092981. + final String protocol = t.getURLName().getProtocol(); + verifyProperties(session, protocol); + String mailHost = session.getProperty("mail." + + protocol + ".host"); + if (isEmpty(mailHost)) { + mailHost = session.getProperty("mail.host"); + } else { + session.getProperty("mail.host"); + } + + local = session.getProperty("mail." + protocol + ".localhost"); + if (isEmpty(local)) { + local = session.getProperty("mail." + + protocol + ".localaddress"); + } else { + session.getProperty("mail." + protocol + ".localaddress"); + } + + if ("resolve".equals(verify)) { + try { //Resolve the remote host name. + String transportHost = t.getURLName().getHost(); + if (!isEmpty(transportHost)) { + verifyHost(transportHost); + if (!transportHost.equalsIgnoreCase(mailHost)) { + verifyHost(mailHost); + } + } else { + verifyHost(mailHost); + } + } catch (final RuntimeException | IOException IOE) { + MessagingException ME = + new MessagingException(msg, IOE); + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + } + } + + if (!"limited".equals(verify)) { + try { //Verify host name and hit the host name cache. + if (!"remote".equals(verify) && !"login".equals(verify)) { + local = getLocalHost(t); + } + verifyHost(local); + } catch (final RuntimeException | IOException IOE) { + MessagingException ME = new MessagingException(msg, IOE); + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + + try { //Verify that the DataHandler can be loaded. + Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { + //Always load the multipart classes. + MimeMultipart multipart = new MimeMultipart(); + MimeBodyPart[] ambp = new MimeBodyPart[atn.length]; + final MimeBodyPart body; + final String bodyContentType; + synchronized (this) { + bodyContentType = contentTypeOf(getFormatter()); + body = createBodyPart(); + for (int i = 0; i < atn.length; ++i) { + ambp[i] = createBodyPart(i); + ambp[i].setFileName(atn[i]); + //Convert names to mime type under lock. + atn[i] = getContentType(atn[i]); + } + } + + body.setDescription(verify); + setContent(body, "", bodyContentType); + multipart.addBodyPart(body); + for (int i = 0; i < ambp.length; ++i) { + ambp[i].setDescription(verify); + setContent(ambp[i], "", atn[i]); + } + + abort.setContent(multipart); + abort.saveChanges(); + abort.writeTo(new ByteArrayOutputStream(MIN_HEADER_SIZE)); + } finally { + getAndSetContextClassLoader(ccl); + } + } catch (final IOException IOE) { + MessagingException ME = new MessagingException(msg, IOE); + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.FORMAT_FAILURE); + } + } + + //Verify all recipients. + if (all.length != 0) { + verifyAddresses(all); + } else { + throw new MessagingException("No recipient addresses."); + } + + //Verify from and sender addresses. + Address[] from = abort.getFrom(); + Address sender = abort.getSender(); + if (sender instanceof InternetAddress) { + ((InternetAddress) sender).validate(); + } + + //If from address is declared then check sender. + if (abort.getHeader("From", ",") != null && from.length != 0) { + verifyAddresses(from); + for (int i = 0; i < from.length; ++i) { + if (from[i].equals(sender)) { + MessagingException ME = new MessagingException( + "Sender address '" + sender + + "' equals from address."); + throw new MessagingException(msg, ME); + } + } + } else { + if (sender == null) { + MessagingException ME = new MessagingException( + "No from or sender address."); + throw new MessagingException(msg, ME); + } + } + + //Verify reply-to addresses. + verifyAddresses(abort.getReplyTo()); + } catch (final RuntimeException RE) { + setErrorContent(abort, verify, RE); + reportError(abort, RE, ErrorManager.OPEN_FAILURE); + } catch (final Exception ME) { + setErrorContent(abort, verify, ME); + reportError(abort, ME, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Handles all exceptions thrown when save changes is called on a message + * that doesn't have any content. + * + * @param abort the message requiring save changes. + * @param msg the error description. + * @since JavaMail 1.6.0 + */ + private void saveChangesNoContent(final Message abort, final String msg) { + if (abort != null) { + try { + try { + abort.saveChanges(); + } catch (final NullPointerException xferEncoding) { + //Workaround GNU JavaMail bug in MimeUtility.getEncoding + //when the mime message has no content. + try { + String cte = "Content-Transfer-Encoding"; + if (abort.getHeader(cte) == null) { + abort.setHeader(cte, "base64"); + abort.saveChanges(); + } else { + throw xferEncoding; + } + } catch (RuntimeException | MessagingException e) { + if (e != xferEncoding) { + e.addSuppressed(xferEncoding); + } + throw e; + } + } + } catch (RuntimeException | MessagingException ME) { + reportError(msg, ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Cache common session properties into the LogManagerProperties. This is + * a workaround for JDK-7092981. + * + * @param session the session. + * @param protocol the mail protocol. + * @throws NullPointerException if session is null. + * @since JavaMail 1.6.0 + */ + private static void verifyProperties(Session session, String protocol) { + session.getProperty("mail.from"); + session.getProperty("mail." + protocol + ".from"); + session.getProperty("mail.dsn.ret"); + session.getProperty("mail." + protocol + ".dsn.ret"); + session.getProperty("mail.dsn.notify"); + session.getProperty("mail." + protocol + ".dsn.notify"); + session.getProperty("mail." + protocol + ".port"); + session.getProperty("mail.user"); + session.getProperty("mail." + protocol + ".user"); + session.getProperty("mail." + protocol + ".localport"); + } + + /** + * Perform a lookup of the host address or FQDN. + * @param host the host or null. + * @return the address. + * @throws IOException if the host name is not valid. + * @throws SecurityException if security manager is present and doesn't + * allow access to check connect permission. + * @since JavaMail 1.5.0 + */ + private static InetAddress verifyHost(String host) throws IOException { + InetAddress a; + if (isEmpty(host)) { + a = InetAddress.getLocalHost(); + } else { + a = InetAddress.getByName(host); + } + if (a.getCanonicalHostName().length() == 0) { + throw new UnknownHostException(); + } + return a; + } + + /** + * Calls validate for every address given. + * If the addresses given are null, empty or not an InternetAddress then + * the check is skipped. + * @param all any address array, null or empty. + * @throws AddressException if there is a problem. + * @since JavaMail 1.4.5 + */ + private static void verifyAddresses(Address[] all) throws AddressException { + if (all != null) { + for (int i = 0; i < all.length; ++i) { + final Address a = all[i]; + if (a instanceof InternetAddress) { + ((InternetAddress) a).validate(); + } + } + } + } + + /** + * Reports that an empty content message was sent and should not have been. + * @param msg the MimeMessage. + * @param verify the verify enum. + * @param cause the exception that caused the problem or null. + * @since JavaMail 1.4.5 + */ + private void reportUnexpectedSend(MimeMessage msg, String verify, Exception cause) { + final MessagingException write = new MessagingException( + "An empty message was sent.", cause); + setErrorContent(msg, verify, write); + reportError(msg, write, ErrorManager.OPEN_FAILURE); + } + + /** + * Creates and sets the message content from the given Throwable. + * When verify fails, this method fixes the 'abort' message so that any + * created envelope data can be used in the error manager. + * @param msg the message with or without content. + * @param verify the verify enum. + * @param t the throwable or null. + * @since JavaMail 1.4.5 + */ + private void setErrorContent(MimeMessage msg, String verify, Throwable t) { + try { //Add content so toRawString doesn't fail. + final MimeBodyPart body; + final String subjectType; + final String msgDesc; + synchronized (this) { + body = createBodyPart(); + msgDesc = descriptionFrom(comparator, pushLevel, pushFilter); + subjectType = getClassId(subjectFormatter); + } + + body.setDescription("Formatted using " + + (t == null ? Throwable.class.getName() + : t.getClass().getName()) + ", filtered with " + + verify + ", and named by " + + subjectType + '.'); + setContent(body, toMsgString(t), "text/plain"); + final MimeMultipart multipart = new MimeMultipart(); + multipart.addBodyPart(body); + msg.setContent(multipart); + msg.setDescription(msgDesc); + setAcceptLang(msg); + msg.saveChanges(); + } catch (MessagingException | RuntimeException ME) { + reportError("Unable to create body.", ME, ErrorManager.OPEN_FAILURE); + } + } + + /** + * Used to update the cached session object based on changes in + * mail properties or authenticator. + * @return the current session or null if no verify is required. + */ + private Session updateSession() { + assert Thread.holdsLock(this); + final Session settings; + if (mailProps.getProperty("verify") != null) { + settings = initSession(); + assert settings == session : session; + } else { + session = null; //Remove old session. + settings = null; + } + return settings; + } + + /** + * Creates a session using a proxy properties object. + * @return the session that was created and assigned. + */ + private Session initSession() { + assert Thread.holdsLock(this); + final String p = getClass().getName(); + LogManagerProperties proxy = new LogManagerProperties(mailProps, p); + session = Session.getInstance(proxy, auth); + return session; + } + + /** + * Creates all of the envelope information for a message. + * This method is safe to call outside of a lock because the message + * provides the safe snapshot of the mail properties. + * @param msg the Message to write the envelope information. + * @param priority true for high priority. + */ + private void envelopeFor(Message msg, boolean priority) { + setAcceptLang(msg); + setFrom(msg); + if (!setRecipient(msg, "mail.to", Message.RecipientType.TO)) { + setDefaultRecipient(msg, Message.RecipientType.TO); + } + setRecipient(msg, "mail.cc", Message.RecipientType.CC); + setRecipient(msg, "mail.bcc", Message.RecipientType.BCC); + setReplyTo(msg); + setSender(msg); + setMailer(msg); + setAutoSubmitted(msg); + if (priority) { + setPriority(msg); + } + + try { + msg.setSentDate(new java.util.Date()); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Factory to create the in-line body part. + * @return a body part with default headers set. + * @throws MessagingException if there is a problem. + */ + private MimeBodyPart createBodyPart() throws MessagingException { + assert Thread.holdsLock(this); + final MimeBodyPart part = new MimeBodyPart(); + part.setDisposition(Part.INLINE); + part.setDescription(descriptionFrom(getFormatter(), + getFilter(), subjectFormatter)); + setAcceptLang(part); + return part; + } + + /** + * Factory to create the attachment body part. + * @param index the attachment index. + * @return a body part with default headers set. + * @throws MessagingException if there is a problem. + * @throws IndexOutOfBoundsException if the given index is not an valid + * attachment index. + */ + private MimeBodyPart createBodyPart(int index) throws MessagingException { + assert Thread.holdsLock(this); + final MimeBodyPart part = new MimeBodyPart(); + part.setDisposition(Part.ATTACHMENT); + part.setDescription(descriptionFrom( + attachmentFormatters[index], + attachmentFilters[index], + attachmentNames[index])); + setAcceptLang(part); + return part; + } + + /** + * Gets the description for the MimeMessage itself. + * The push level and filter are included because they play a role in + * formatting of a message when triggered or not triggered. + * @param c the comparator. + * @param l the pushLevel. + * @param f the pushFilter + * @return the description. + * @throws NullPointerException if level is null. + * @since JavaMail 1.4.5 + */ + private String descriptionFrom(Comparator c, Level l, Filter f) { + return "Sorted using "+ (c == null ? "no comparator" + : c.getClass().getName()) + ", pushed when "+ l.getName() + + ", and " + (f == null ? "no push filter" + : f.getClass().getName()) + '.'; + } + + /** + * Creates a description for a body part. + * @param f the content formatter. + * @param filter the content filter. + * @param name the naming formatter. + * @return the description for the body part. + */ + private String descriptionFrom(Formatter f, Filter filter, Formatter name) { + return "Formatted using " + getClassId(f) + + ", filtered with " + (filter == null ? "no filter" + : filter.getClass().getName()) +", and named by " + + getClassId(name) + '.'; + } + + /** + * Gets a class name represents the behavior of the formatter. + * The class name may not be assignable to a Formatter. + * @param f the formatter. + * @return a class name that represents the given formatter. + * @throws NullPointerException if the parameter is null. + * @since JavaMail 1.4.5 + */ + private String getClassId(final Formatter f) { + if (f instanceof TailNameFormatter) { + return String.class.getName(); //Literal string. + } else { + return f.getClass().getName(); + } + } + + /** + * Ensure that a formatter creates a valid string for a part name. + * @param f the formatter. + * @return the to string value or the class name. + */ + private String toString(final Formatter f) { + //Should never be null but, guard against formatter bugs. + final String name = f.toString(); + if (!isEmpty(name)) { + return name; + } else { + return getClassId(f); + } + } + + /** + * Constructs a file name from a formatter. This method is called often + * but, rarely does any work. + * @param part to append to. + * @param chunk non null string to append. + */ + private void appendFileName(final Part part, final String chunk) { + if (chunk != null) { + if (chunk.length() > 0) { + appendFileName0(part, chunk); + } + } else { + reportNullError(ErrorManager.FORMAT_FAILURE); + } + } + + /** + * It is assumed that file names are short and that in most cases + * getTail will be the only method that will produce a result. + * @param part to append to. + * @param chunk non null string to append. + */ + private void appendFileName0(final Part part, String chunk) { + try { + //Remove all control character groups. + chunk = chunk.replaceAll("[\\x00-\\x1F\\x7F]+", ""); + final String old = part.getFileName(); + part.setFileName(old != null ? old.concat(chunk) : chunk); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Constructs a subject line from a formatter. + * @param msg to append to. + * @param chunk non null string to append. + */ + private void appendSubject(final Message msg, final String chunk) { + if (chunk != null) { + if (chunk.length() > 0) { + appendSubject0(msg, chunk); + } + } else { + reportNullError(ErrorManager.FORMAT_FAILURE); + } + } + + /** + * It is assumed that subject lines are short and that in most cases + * getTail will be the only method that will produce a result. + * @param msg to append to. + * @param chunk non null string to append. + */ + private void appendSubject0(final Message msg, String chunk) { + try { + //Remove all control character groups. + chunk = chunk.replaceAll("[\\x00-\\x1F\\x7F]+", ""); + final String charset = getEncodingName(); + final String old = msg.getSubject(); + assert msg instanceof MimeMessage : msg; + ((MimeMessage) msg).setSubject(old != null ? old.concat(chunk) + : chunk, MimeUtility.mimeCharset(charset)); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Gets the locale for the given log record from the resource bundle. + * If the resource bundle is using the root locale then the default locale + * is returned. + * @param r the log record. + * @return null if not localized otherwise, the locale of the record. + * @since JavaMail 1.4.5 + */ + private Locale localeFor(final LogRecord r) { + Locale l; + final ResourceBundle rb = r.getResourceBundle(); + if (rb != null) { + l = rb.getLocale(); + if (l == null || isEmpty(l.getLanguage())) { + //The language of the fallback bundle (root) is unknown. + //1. Use default locale. Should only be wrong if the app is + // used with a langauge that was unintended. (unlikely) + //2. Mark it as not localized (force null, info loss). + //3. Use the bundle name (encoded) as an experimental language. + l = Locale.getDefault(); + } + } else { + l = null; + } + return l; + } + + /** + * Appends the content language to the given mime part. + * The language tag is only appended if the given language has not been + * specified. This method is only used when we have LogRecords that are + * localized with an assigned resource bundle. + * @param p the mime part. + * @param l the locale to append. + * @throws NullPointerException if any argument is null. + * @since JavaMail 1.4.5 + */ + private void appendContentLang(final MimePart p, final Locale l) { + try { + String lang = LogManagerProperties.toLanguageTag(l); + if (lang.length() != 0) { + String header = p.getHeader("Content-Language", null); + if (isEmpty(header)) { + p.setHeader("Content-Language", lang); + } else if (!header.equalsIgnoreCase(lang)) { + lang = ",".concat(lang); + int idx = 0; + while ((idx = header.indexOf(lang, idx)) > -1) { + idx += lang.length(); + if (idx == header.length() + || header.charAt(idx) == ',') { + break; + } + } + + if (idx < 0) { + int len = header.lastIndexOf("\r\n\t"); + if (len < 0) { //If not folded. + len = (18 + 2) + header.length(); + } else { + len = (header.length() - len) + 8; + } + + //Perform folding of header if needed. + if ((len + lang.length()) > 76) { + header = header.concat("\r\n\t".concat(lang)); + } else { + header = header.concat(lang); + } + p.setHeader("Content-Language", header); + } + } + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Sets the accept language to the default locale of the JVM. + * If the locale is the root locale the header is not added. + * @param p the part to set. + * @since JavaMail 1.4.5 + */ + private void setAcceptLang(final Part p) { + try { + final String lang = LogManagerProperties + .toLanguageTag(Locale.getDefault()); + if (lang.length() != 0) { + p.setHeader("Accept-Language", lang); + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Used when a log record was loggable prior to being inserted + * into the buffer but at the time of formatting was no longer loggable. + * Filters were changed after publish but prior to a push or a bug in the + * body filter or one of the attachment filters. + * @param record that was not formatted. + * @since JavaMail 1.4.5 + */ + private void reportFilterError(final LogRecord record) { + assert Thread.holdsLock(this); + final Formatter f = createSimpleFormatter(); + final String msg = "Log record " + record.getSequenceNumber() + + " was filtered from all message parts. " + + head(f) + format(f, record) + tail(f, ""); + final String txt = getFilter() + ", " + + Arrays.asList(readOnlyAttachmentFilters()); + reportError(msg, new IllegalArgumentException(txt), + ErrorManager.FORMAT_FAILURE); + } + + /** + * Reports symmetric contract violations an equals implementation. + * @param o the test object must be non null. + * @param found the possible intern, must be non null. + * @throws NullPointerException if any argument is null. + * @since JavaMail 1.5.0 + */ + private void reportNonSymmetric(final Object o, final Object found) { + reportError("Non symmetric equals implementation." + , new IllegalArgumentException(o.getClass().getName() + + " is not equal to " + found.getClass().getName()) + , ErrorManager.OPEN_FAILURE); + } + + /** + * Reports equals implementations that do not discriminate between objects + * of different types or subclass types. + * @param o the test object must be non null. + * @param found the possible intern, must be non null. + * @throws NullPointerException if any argument is null. + * @since JavaMail 1.5.0 + */ + private void reportNonDiscriminating(final Object o, final Object found) { + reportError("Non discriminating equals implementation." + , new IllegalArgumentException(o.getClass().getName() + + " should not be equal to " + found.getClass().getName()) + , ErrorManager.OPEN_FAILURE); + } + + /** + * Used to outline the bytes to report a null pointer exception. + * See BUD ID 6533165. + * @param code the ErrorManager code. + */ + private void reportNullError(final int code) { + reportError("null", new NullPointerException(), code); + } + + /** + * Creates the head or reports a formatting error. + * @param f the formatter. + * @return the head string or an empty string. + */ + private String head(final Formatter f) { + try { + return f.getHead(this); + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + return ""; + } + } + + /** + * Creates the formatted log record or reports a formatting error. + * @param f the formatter. + * @param r the log record. + * @return the formatted string or an empty string. + */ + private String format(final Formatter f, final LogRecord r) { + try { + return f.format(r); + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + return ""; + } + } + + /** + * Creates the tail or reports a formatting error. + * @param f the formatter. + * @param def the default string to use when there is an error. + * @return the tail string or the given default string. + */ + private String tail(final Formatter f, final String def) { + try { + return f.getTail(this); + } catch (final RuntimeException RE) { + reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); + return def; + } + } + + /** + * Sets the x-mailer header. + * @param msg the target message. + */ + private void setMailer(final Message msg) { + try { + final Class mail = MailHandler.class; + final Class k = getClass(); + String value; + if (k == mail) { + value = mail.getName(); + } else { + try { + value = MimeUtility.encodeText(k.getName()); + } catch (final UnsupportedEncodingException E) { + reportError(E.getMessage(), E, ErrorManager.FORMAT_FAILURE); + value = k.getName().replaceAll("[^\\x00-\\x7F]", "\uu001A"); + } + value = MimeUtility.fold(10, mail.getName() + " using the " + + value + " extension."); + } + msg.setHeader("X-Mailer", value); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Sets the priority and importance headers. + * @param msg the target message. + */ + private void setPriority(final Message msg) { + try { + msg.setHeader("Importance", "High"); + msg.setHeader("Priority", "urgent"); + msg.setHeader("X-Priority", "2"); //High + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Used to signal that body parts are missing from a message. Also used + * when LogRecords were passed to an attachment formatter but the formatter + * produced no output, which is allowed. Used during a verify because all + * parts are omitted, none of the content formatters are used. This is + * not used when a filter prevents LogRecords from being formatted. + * This header is defined in RFC 2156 and RFC 4021. + * @param msg the message. + * @since JavaMail 1.4.5 + */ + private void setIncompleteCopy(final Message msg) { + try { + msg.setHeader("Incomplete-Copy", ""); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Signals that this message was generated by automatic process. + * This header is defined in RFC 3834 section 5. + * @param msg the message. + * @since JavaMail 1.4.6 + */ + private void setAutoSubmitted(final Message msg) { + if (allowRestrictedHeaders()) { + try { //RFC 3834 (5.2) + msg.setHeader("auto-submitted", "auto-generated"); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Sets from address header. + * @param msg the target message. + */ + private void setFrom(final Message msg) { + final String from = getSession(msg).getProperty("mail.from"); + if (from != null) { + try { + final Address[] address = InternetAddress.parse(from, false); + if (address.length > 0) { + if (address.length == 1) { + msg.setFrom(address[0]); + } else { //Greater than 1 address. + msg.addFrom(address); + } + } + //Can't place an else statement here because the 'from' is + //not null which causes the local address computation + //to fail. Assume the user wants to omit the from address + //header. + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + setDefaultFrom(msg); + } + } else { + setDefaultFrom(msg); + } + } + + /** + * Sets the from header to the local address. + * @param msg the target message. + */ + private void setDefaultFrom(final Message msg) { + try { + msg.setFrom(); + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Computes the default to-address if none was specified. This can + * fail if the local address can't be computed. + * @param msg the message + * @param type the recipient type. + * @since JavaMail 1.5.0 + */ + private void setDefaultRecipient(final Message msg, + final Message.RecipientType type) { + try { + Address a = InternetAddress.getLocalAddress(getSession(msg)); + if (a != null) { + msg.setRecipient(type, a); + } else { + final MimeMessage m = new MimeMessage(getSession(msg)); + m.setFrom(); //Should throw an exception with a cause. + Address[] from = m.getFrom(); + if (from.length > 0) { + msg.setRecipients(type, from); + } else { + throw new MessagingException("No local address."); + } + } + } catch (MessagingException | RuntimeException ME) { + reportError("Unable to compute a default recipient.", + ME, ErrorManager.FORMAT_FAILURE); + } + } + + /** + * Sets reply-to address header. + * @param msg the target message. + */ + private void setReplyTo(final Message msg) { + final String reply = getSession(msg).getProperty("mail.reply.to"); + if (!isEmpty(reply)) { + try { + final Address[] address = InternetAddress.parse(reply, false); + if (address.length > 0) { + msg.setReplyTo(address); + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * Sets sender address header. + * @param msg the target message. + */ + private void setSender(final Message msg) { + assert msg instanceof MimeMessage : msg; + final String sender = getSession(msg).getProperty("mail.sender"); + if (!isEmpty(sender)) { + try { + final InternetAddress[] address = + InternetAddress.parse(sender, false); + if (address.length > 0) { + ((MimeMessage) msg).setSender(address[0]); + if (address.length > 1) { + reportError("Ignoring other senders.", + tooManyAddresses(address, 1), + ErrorManager.FORMAT_FAILURE); + } + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + } + + /** + * A common factory used to create the too many addresses exception. + * @param address the addresses, never null. + * @param offset the starting address to display. + * @return the too many addresses exception. + */ + private AddressException tooManyAddresses(Address[] address, int offset) { + Object l = Arrays.asList(address).subList(offset, address.length); + return new AddressException(l.toString()); + } + + /** + * Sets the recipient for the given message. + * @param msg the message. + * @param key the key to search in the session. + * @param type the recipient type. + * @return true if the key was contained in the session. + */ + private boolean setRecipient(final Message msg, + final String key, final Message.RecipientType type) { + boolean containsKey; + final String value = getSession(msg).getProperty(key); + containsKey = value != null; + if (!isEmpty(value)) { + try { + final Address[] address = InternetAddress.parse(value, false); + if (address.length > 0) { + msg.setRecipients(type, address); + } + } catch (final MessagingException ME) { + reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); + } + } + return containsKey; + } + + /** + * Converts an email message to a raw string. This raw string + * is passed to the error manager to allow custom error managers + * to recreate the original MimeMessage object. + * @param msg a Message object. + * @return the raw string or null if msg was null. + * @throws MessagingException if there was a problem with the message. + * @throws IOException if there was a problem. + */ + private String toRawString(final Message msg) throws MessagingException, IOException { + if (msg != null) { + Object ccl = getAndSetContextClassLoader(MAILHANDLER_LOADER); + try { //JDK-8025251 + int nbytes = Math.max(msg.getSize() + MIN_HEADER_SIZE, MIN_HEADER_SIZE); + ByteArrayOutputStream out = new ByteArrayOutputStream(nbytes); + msg.writeTo(out); //Headers can be UTF-8 or US-ASCII. + return out.toString("UTF-8"); + } finally { + getAndSetContextClassLoader(ccl); + } + } else { //Must match this.reportError behavior, see push method. + return null; //Null is the safe choice. + } + } + + /** + * Converts a throwable to a message string. + * @param t any throwable or null. + * @return the throwable with a stack trace or the literal null. + */ + private String toMsgString(final Throwable t) { + if (t == null) { + return "null"; + } + + final String charset = getEncodingName(); + try { + final ByteArrayOutputStream out = + new ByteArrayOutputStream(MIN_HEADER_SIZE); + + //Create an output stream writer so streams are not double buffered. + try (OutputStreamWriter ows = new OutputStreamWriter(out, charset); + PrintWriter pw = new PrintWriter(ows)) { + pw.println(t.getMessage()); + t.printStackTrace(pw); + pw.flush(); + } //Close OSW before generating string. JDK-6995537 + return out.toString(charset); + } catch (final RuntimeException unexpected) { + return t.toString() + ' ' + unexpected.toString(); + } catch (final Exception badMimeCharset) { + return t.toString() + ' ' + badMimeCharset.toString(); + } + } + + /** + * Replaces the current context class loader with our class loader. + * @param ccl null for boot class loader, a class loader, a class used to + * get the class loader, or a source object to get the class loader. + * @return null for the boot class loader, a class loader, or a marker + * object to signal that no modification was required. + * @since JavaMail 1.5.3 + */ + private Object getAndSetContextClassLoader(final Object ccl) { + if (ccl != GetAndSetContext.NOT_MODIFIED) { + try { + final PrivilegedAction pa; + if (ccl instanceof PrivilegedAction) { + pa = (PrivilegedAction) ccl; + } else { + pa = new GetAndSetContext(ccl); + } + return AccessController.doPrivileged(pa); + } catch (final SecurityException ignore) { + } + } + return GetAndSetContext.NOT_MODIFIED; + } + + /** + * A factory used to create a common attachment mismatch type. + * @param msg the exception message. + * @return a RuntimeException to represent the type of error. + */ + private static RuntimeException attachmentMismatch(final String msg) { + return new IndexOutOfBoundsException(msg); + } + + /** + * Outline the attachment mismatch message. See Bug ID 6533165. + * @param expected the expected array length. + * @param found the array length that was given. + * @return a RuntimeException populated with a message. + */ + private static RuntimeException attachmentMismatch(int expected, int found) { + return attachmentMismatch("Attachments mismatched, expected " + + expected + " but given " + found + '.'); + } + + /** + * Try to attach a suppressed exception to a MessagingException in any order + * that is possible. + * @param required the exception expected to see as a reported failure. + * @param optional the suppressed exception. + * @return either the required or the optional exception. + */ + private static MessagingException attach( + MessagingException required, Exception optional) { + if (optional != null && !required.setNextException(optional)) { + if (optional instanceof MessagingException) { + final MessagingException head = (MessagingException) optional; + if (head.setNextException(required)) { + return head; + } + } + + if (optional != required) { + required.addSuppressed(optional); + } + } + return required; + } + + /** + * Gets the local host from the given service object. + * @param s the service to check. + * @return the local host or null. + * @since JavaMail 1.5.3 + */ + private String getLocalHost(final Service s) { + try { + return LogManagerProperties.getLocalHost(s); + } catch (SecurityException | NoSuchMethodException + | LinkageError ignore) { + } catch (final Exception ex) { + reportError(s.toString(), ex, ErrorManager.OPEN_FAILURE); + } + return null; + } + + /** + * Google App Engine doesn't support Message.getSession. + * @param msg the message. + * @return the session from the given message. + * @throws NullPointerException if the given message is null. + * @since JavaMail 1.5.3 + */ + private Session getSession(final Message msg) { + if (msg == null) { + throw new NullPointerException(); + } + return new MessageContext(msg).getSession(); + } + + /** + * Determines if restricted headers are allowed in the current environment. + * + * @return true if restricted headers are allowed. + * @since JavaMail 1.5.3 + */ + private boolean allowRestrictedHeaders() { + //GAE will prevent delivery of email with forbidden headers. + //Assume the environment is GAE if access to the LogManager is + //forbidden. + return LogManagerProperties.hasLogManager(); + } + + /** + * Outline the creation of the index error message. See JDK-6533165. + * @param i the index. + * @return the error message. + */ + private static String atIndexMsg(final int i) { + return "At index: " + i + '.'; + } + + /** + * Used for storing a password from the LogManager or literal string. + * @since JavaMail 1.4.6 + */ + private static final class DefaultAuthenticator extends Authenticator { + + /** + * Creates an Authenticator for the given password. This method is used + * so class verification of assignments in MailHandler doesn't require + * loading this class which otherwise can occur when using the + * constructor. Default access to avoid generating extra class files. + * + * @param pass the password. + * @return an Authenticator for the password. + * @since JavaMail 1.5.6 + */ + static Authenticator of(final String pass) { + return new DefaultAuthenticator(pass); + } + + /** + * The password to use. + */ + private final String pass; + + /** + * Use the factory method instead of this constructor. + * @param pass the password. + */ + private DefaultAuthenticator(final String pass) { + assert pass != null; + this.pass = pass; + } + + @Override + protected final PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(getDefaultUserName(), pass); + } + } + + /** + * Performs a get and set of the context class loader with privileges + * enabled. + * @since JavaMail 1.4.6 + */ + private static final class GetAndSetContext implements PrivilegedAction { + /** + * A marker object used to signal that the class loader was not + * modified. + */ + public static final Object NOT_MODIFIED = GetAndSetContext.class; + /** + * The source containing the class loader. + */ + private final Object source; + /** + * Create the action. + * @param source null for boot class loader, a class loader, a class + * used to get the class loader, or a source object to get the class + * loader. Default access to avoid generating extra class files. + */ + GetAndSetContext(final Object source) { + this.source = source; + } + + /** + * Gets the class loader from the source and sets the CCL only if + * the source and CCL are not the same. + * @return the replaced context class loader which can be null or + * NOT_MODIFIED to indicate that nothing was modified. + */ + @SuppressWarnings("override") //JDK-6954234 + public final Object run() { + final Thread current = Thread.currentThread(); + final ClassLoader ccl = current.getContextClassLoader(); + final ClassLoader loader; + if (source == null) { + loader = null; //boot class loader + } else if (source instanceof ClassLoader) { + loader = (ClassLoader) source; + } else if (source instanceof Class) { + loader = ((Class) source).getClassLoader(); + } else if (source instanceof Thread) { + loader = ((Thread) source).getContextClassLoader(); + } else { + assert !(source instanceof Class) : source; + loader = source.getClass().getClassLoader(); + } + + if (ccl != loader) { + current.setContextClassLoader(loader); + return ccl; + } else { + return NOT_MODIFIED; + } + } + } + + /** + * Used for naming attachment file names and the main subject line. + */ + private static final class TailNameFormatter extends Formatter { + + /** + * Creates or gets a formatter from the given name. This method is used + * so class verification of assignments in MailHandler doesn't require + * loading this class which otherwise can occur when using the + * constructor. Default access to avoid generating extra class files. + * + * @param name any not null string. + * @return a formatter for that string. + * @since JavaMail 1.5.6 + */ + static Formatter of(final String name) { + return new TailNameFormatter(name); + } + + /** + * The value used as the output. + */ + private final String name; + + /** + * Use the factory method instead of this constructor. + * @param name any not null string. + */ + private TailNameFormatter(final String name) { + assert name != null; + this.name = name; + } + + @Override + public final String format(LogRecord record) { + return ""; + } + + @Override + public final String getTail(Handler h) { + return name; + } + + /** + * Equals method. + * @param o the other object. + * @return true if equal + * @since JavaMail 1.4.4 + */ + @Override + public final boolean equals(Object o) { + if (o instanceof TailNameFormatter) { + return name.equals(((TailNameFormatter) o).name); + } + return false; + } + + /** + * Hash code method. + * @return the hash code. + * @since JavaMail 1.4.4 + */ + @Override + public final int hashCode() { + return getClass().hashCode() + name.hashCode(); + } + + @Override + public final String toString() { + return name; + } + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/SeverityComparator.java b/app/src/main/java/com/sun/mail/util/logging/SeverityComparator.java new file mode 100644 index 0000000000..338a780bff --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/SeverityComparator.java @@ -0,0 +1,332 @@ +/* + * Copyright (c) 2013, 2018 Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2013, 2018 Jason Mehrens. All rights reserved. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v. 2.0, which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * This Source Code may also be made available under the following Secondary + * Licenses when the conditions for such availability set forth in the + * Eclipse Public License v. 2.0 are satisfied: GNU General Public License, + * version 2 with the GNU Classpath Exception, which is available at + * https://www.gnu.org/software/classpath/license.html. + * + * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0 + */ +package com.sun.mail.util.logging; + +import java.io.Serializable; +import java.util.Comparator; +import java.util.logging.Level; +import java.util.logging.LogRecord; + +/** + * Orders log records by level, thrown, sequence, and time. + * + * This comparator orders LogRecords by how severely each is attributed to + * failures in a program. The primary ordering is determined by the use of the + * logging API throughout a program by specifying a level to each log message. + * The secondary ordering is determined at runtime by the type of errors and + * exceptions generated by the program. The remaining ordering assumes that + * older log records are less severe than newer log records. + * + *

+ * The following LogRecord properties determine severity ordering: + *

    + *
  1. The natural comparison of the LogRecord + * {@linkplain Level#intValue level}. + *
  2. The expected recovery order of {@linkplain LogRecord#getThrown() thrown} + * property of a LogRecord and its cause chain. This ordering is derived from + * the JLS 11.1.1. The Kinds of Exceptions and JLS 11.5 The Exception Hierarchy. + * This is performed by {@linkplain #apply(java.lang.Throwable) finding} the + * throwable that best describes the entire cause chain. Once a specific + * throwable of each chain is identified it is then ranked lowest to highest by + * the following rules: + * + *
      + *
    • All LogRecords with a {@code Throwable} defined as + * "{@link #isNormal(java.lang.Throwable) normal occurrence}". + *
    • All LogRecords that do not have a thrown object. + *
    • All checked exceptions. This is any class that is assignable to the + * {@code java.lang.Throwable} class and is not a + * {@code java.lang.RuntimeException} or a {@code java.lang.Error}. + *
    • All unchecked exceptions. This is all {@code java.lang.RuntimeException} + * objects. + *
    • All errors that indicate a serious problem. This is all + * {@code java.lang.Error} objects. + *
    + *
  3. The natural comparison of the LogRecord + * {@linkplain LogRecord#getSequenceNumber() sequence}. + *
  4. The natural comparison of the LogRecord + * {@linkplain LogRecord#getMillis() millis}. + *
+ * + * @author Jason Mehrens + * @since JavaMail 1.5.2 + */ +public class SeverityComparator implements Comparator, Serializable { + + /** + * The generated serial version UID. + */ + private static final long serialVersionUID = -2620442245251791965L; + + /** + * A single instance that is shared among the logging package. + * The field is declared as java.util.Comparator so + * WebappClassLoader.clearReferencesStaticFinal() method will ignore this + * field. + */ + private static final Comparator INSTANCE + = new SeverityComparator(); + + /** + * A shared instance of a SeverityComparator. This is package private so the + * public API is not polluted with more methods. + * + * @return a shared instance of a SeverityComparator. + */ + static SeverityComparator getInstance() { + return (SeverityComparator) INSTANCE; + } + + /** + * Identifies a single throwable that best describes the given throwable and + * the entire {@linkplain Throwable#getCause() cause} chain. This method can + * be overridden to change the behavior of + * {@link #compare(java.util.logging.LogRecord, java.util.logging.LogRecord)}. + * + * @param chain the throwable or null. + * @return null if null was given, otherwise the throwable that best + * describes the entire chain. + * @see #isNormal(java.lang.Throwable) + */ + public Throwable apply(final Throwable chain) { + //Matches the j.u.f.UnaryOperator interface. + int limit = 0; + Throwable root = chain; + Throwable high = null; + Throwable normal = null; + for (Throwable cause = chain; cause != null; cause = cause.getCause()) { + root = cause; //Find the deepest cause. + + //Find the deepest nomral occurrance. + if (isNormal(cause)) { + normal = cause; + } + + //Find the deepest error that happened before a normal occurance. + if (normal == null && cause instanceof Error) { + high = cause; + } + + //Deal with excessive cause chains and cyclic throwables. + if (++limit == (1 << 16)) { + break; //Give up. + } + } + return high != null ? high : normal != null ? normal : root; + } + + /** + * {@link #apply(java.lang.Throwable) Reduces} each throwable chain argument + * then compare each throwable result. + * + * @param tc1 the first throwable chain or null. + * @param tc2 the second throwable chain or null. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + * @see #apply(java.lang.Throwable) + * @see #compareThrowable(java.lang.Throwable, java.lang.Throwable) + */ + public final int applyThenCompare(Throwable tc1, Throwable tc2) { + return tc1 == tc2 ? 0 : compareThrowable(apply(tc1), apply(tc2)); + } + + /** + * Compares two throwable objects or null. This method does not + * {@link #apply(java.lang.Throwable) reduce} each argument before + * comparing. This is method can be overridden to change the behavior of + * {@linkplain #compare(LogRecord, LogRecord)}. + * + * @param t1 the first throwable or null. + * @param t2 the second throwable or null. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + * @see #isNormal(java.lang.Throwable) + */ + public int compareThrowable(final Throwable t1, final Throwable t2) { + if (t1 == t2) { //Reflexive test including null. + return 0; + } else { + //Only one or the other is null at this point. + //Force normal occurrence to be lower than null. + if (t1 == null) { + return isNormal(t2) ? 1 : -1; + } else { + if (t2 == null) { + return isNormal(t1) ? -1 : 1; + } + } + + //From this point on neither are null. + //Follow the shortcut if we can. + if (t1.getClass() == t2.getClass()) { + return 0; + } + + //Ensure normal occurrence flow control is ordered low. + if (isNormal(t1)) { + return isNormal(t2) ? 0 : -1; + } else { + if (isNormal(t2)) { + return 1; + } + } + + //Rank the two unidenticial throwables using the rules from + //JLS 11.1.1. The Kinds of Exceptions and + //JLS 11.5 The Exception Hierarchy. + if (t1 instanceof Error) { + return t2 instanceof Error ? 0 : 1; + } else if (t1 instanceof RuntimeException) { + return t2 instanceof Error ? -1 + : t2 instanceof RuntimeException ? 0 : 1; + } else { + return t2 instanceof Error + || t2 instanceof RuntimeException ? -1 : 0; + } + } + } + + /** + * Compares two log records based on severity. + * + * @param o1 the first log record. + * @param o2 the second log record. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + * @throws NullPointerException if either argument is null. + */ + @SuppressWarnings("override") //JDK-6954234 + public int compare(final LogRecord o1, final LogRecord o2) { + if (o1 == null || o2 == null) { //Don't allow null. + throw new NullPointerException(toString(o1, o2)); + } + + /** + * LogRecords are mutable so a reflexive relationship test is a safety + * requirement. + */ + if (o1 == o2) { + return 0; + } + + int cmp = compare(o1.getLevel(), o2.getLevel()); + if (cmp == 0) { + cmp = applyThenCompare(o1.getThrown(), o2.getThrown()); + if (cmp == 0) { + cmp = compare(o1.getSequenceNumber(), o2.getSequenceNumber()); + if (cmp == 0) { + cmp = compare(o1.getMillis(), o2.getMillis()); + } + } + } + return cmp; + } + + /** + * Determines if the given object is also a comparator and it imposes the + * same ordering as this comparator. + * + * @param o the reference object with which to compare. + * @return true if this object equal to the argument; false otherwise. + */ + @Override + public boolean equals(final Object o) { + return o == null ? false : o.getClass() == getClass(); + } + + /** + * Returns a hash code value for the object. + * + * @return Returns a hash code value for the object. + */ + @Override + public int hashCode() { + return 31 * getClass().hashCode(); + } + + /** + * Determines if the given throwable instance is "normal occurrence". This + * is any checked or unchecked exception with 'Interrupt' in the class name + * or ancestral class name. Any {@code java.lang.ThreadDeath} object or + * subclasses. + * + * This method can be overridden to change the behavior of the + * {@linkplain #apply(java.lang.Throwable)} method. + * + * @param t a throwable or null. + * @return true the given throwable is a "normal occurrence". + */ + public boolean isNormal(final Throwable t) { + if (t == null) { //This is only needed when called directly. + return false; + } + + /** + * Use the class names to avoid loading more classes. + */ + final Class root = Throwable.class; + final Class error = Error.class; + for (Class c = t.getClass(); c != root; c = c.getSuperclass()) { + if (error.isAssignableFrom(c)) { + if (c.getName().equals("java.lang.ThreadDeath")) { + return true; + } + } else { + //Interrupt, Interrupted or Interruption. + if (c.getName().contains("Interrupt")) { + return true; + } + } + } + return false; + } + + /** + * Compare two level objects. + * + * @param a the first level. + * @param b the second level. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + */ + private int compare(final Level a, final Level b) { + return a == b ? 0 : compare(a.intValue(), b.intValue()); + } + + /** + * Outline the message create string. + * + * @param o1 argument one. + * @param o2 argument two. + * @return the message string. + */ + private static String toString(final Object o1, final Object o2) { + return o1 + ", " + o2; + } + + /** + * Compare two longs. Can be removed when JDK 1.7 is required. + * + * @param x the first long. + * @param y the second long. + * @return a negative integer, zero, or a positive integer as the first + * argument is less than, equal to, or greater than the second. + */ + private int compare(final long x, final long y) { + return x < y ? -1 : x > y ? 1 : 0; + } +} diff --git a/app/src/main/java/com/sun/mail/util/logging/package.html b/app/src/main/java/com/sun/mail/util/logging/package.html new file mode 100644 index 0000000000..fcde4a4ac3 --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/logging/package.html @@ -0,0 +1,34 @@ + + + + + + + + + + Contains Jakarta Mail extensions for + the Java™ platform's core logging + facilities. This package contains classes used to export log messages + as a formatted email message. Classes in this package typically use + LogManager properties to set default values; see the specific + documentation for each concrete class. + + diff --git a/app/src/main/java/com/sun/mail/util/package.html b/app/src/main/java/com/sun/mail/util/package.html new file mode 100644 index 0000000000..6f809d1f5c --- /dev/null +++ b/app/src/main/java/com/sun/mail/util/package.html @@ -0,0 +1,61 @@ + + + + + + +com.sun.mail.util package + + + +

+Utility classes for use with the Jakarta Mail API. +These utility classes are not part of the Jakarta Mail specification. +While this package contains many classes used by the Jakarta Mail implementation +and not intended for direct use by applications, the classes documented +here may be of use to applications. +

+

+Classes in this package log debugging information using +{@link java.util.logging} as described in the following table: +

+ + + + + + + + + + + + + +
com.sun.mail.util Loggers
Logger NameLogging LevelPurpose
com.sun.mail.util.socketFINERDebugging output related to creating sockets
+ +

+WARNING: The APIs in this package should be +considered EXPERIMENTAL. They may be changed in the +future in ways that are incompatible with applications using the +current APIs. +

+ + + diff --git a/app/src/main/java/eu/faircode/email/ActivityBase.java b/app/src/main/java/eu/faircode/email/ActivityBase.java index 444bd312a0..95120a1933 100644 --- a/app/src/main/java/eu/faircode/email/ActivityBase.java +++ b/app/src/main/java/eu/faircode/email/ActivityBase.java @@ -386,7 +386,7 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc try { super.startActivity(intent); } catch (ActivityNotFoundException ex) { - Log.e(ex); + Log.w(ex); ToastEx.makeText(this, getString(R.string.title_no_viewer, intent.getAction()), Toast.LENGTH_LONG).show(); } } @@ -396,7 +396,7 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc try { super.startActivityForResult(intent, requestCode); } catch (ActivityNotFoundException ex) { - Log.e(ex); + Log.w(ex); ToastEx.makeText(this, getString(R.string.title_no_viewer, intent.getAction()), Toast.LENGTH_LONG).show(); } } @@ -531,6 +531,16 @@ abstract class ActivityBase extends AppCompatActivity implements SharedPreferenc return false; } + @Override + public boolean shouldUpRecreateTask(Intent targetIntent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + ComponentName cn = targetIntent.getComponent(); + if (cn != null && BuildConfig.APPLICATION_ID.equals(cn.getPackageName())) + return false; + } + return super.shouldUpRecreateTask(targetIntent); + } + public interface IKeyPressedListener { boolean onKeyPressed(KeyEvent event); diff --git a/app/src/main/java/eu/faircode/email/ActivityEML.java b/app/src/main/java/eu/faircode/email/ActivityEML.java index 31d85ec5e8..1c754b4683 100644 --- a/app/src/main/java/eu/faircode/email/ActivityEML.java +++ b/app/src/main/java/eu/faircode/email/ActivityEML.java @@ -252,11 +252,10 @@ public class ActivityEML extends ActivityBase { if (!TextUtils.isEmpty(apart.attachment.name)) create.putExtra(Intent.EXTRA_TITLE, apart.attachment.name); Helper.openAdvanced(create); - if (create.resolveActivity(getPackageManager()) == null) + if (create.resolveActivity(getPackageManager()) == null) // system whitelisted ToastEx.makeText(ActivityEML.this, R.string.title_no_saf, Toast.LENGTH_LONG).show(); else startActivityForResult(Helper.getChooser(ActivityEML.this, create), REQUEST_ATTACHMENT); - } }); rvAttachment.setAdapter(adapter); diff --git a/app/src/main/java/eu/faircode/email/ActivitySetup.java b/app/src/main/java/eu/faircode/email/ActivitySetup.java index bb3eb31095..0cf0590c0b 100644 --- a/app/src/main/java/eu/faircode/email/ActivitySetup.java +++ b/app/src/main/java/eu/faircode/email/ActivitySetup.java @@ -410,7 +410,7 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac private void askPassword(final boolean export) { Intent intent = (export ? getIntentExport() : getIntentImport()); - if (intent.resolveActivity(getPackageManager()) == null) { + if (intent.resolveActivity(getPackageManager()) == null) { // // system/GET_CONTENT whitelisted ToastEx.makeText(this, R.string.title_no_saf, Toast.LENGTH_LONG).show(); return; } @@ -1266,7 +1266,7 @@ public class ActivitySetup extends ActivityBase implements FragmentManager.OnBac Intent open = new Intent(Intent.ACTION_GET_CONTENT); open.addCategory(Intent.CATEGORY_OPENABLE); open.setType("*/*"); - if (open.resolveActivity(getPackageManager()) == null) + if (open.resolveActivity(getPackageManager()) == null) // system whitelisted ToastEx.makeText(this, R.string.title_no_saf, Toast.LENGTH_LONG).show(); else startActivityForResult(Helper.getChooser(this, open), REQUEST_IMPORT_CERTIFICATE); diff --git a/app/src/main/java/eu/faircode/email/ActivityView.java b/app/src/main/java/eu/faircode/email/ActivityView.java index a5372a51ad..815c16fe7f 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -489,15 +489,14 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB } }).setExternal(true)); - if (Helper.getIntentIssue(this).resolveActivity(pm) != null) - extra.add(new NavMenuItem(R.drawable.baseline_feedback_24, R.string.menu_issue, new Runnable() { - @Override - public void run() { - if (!drawerLayout.isLocked(drawerContainer)) - drawerLayout.closeDrawer(drawerContainer); - onMenuIssue(); - } - }).setExternal(true)); + extra.add(new NavMenuItem(R.drawable.baseline_feedback_24, R.string.menu_issue, new Runnable() { + @Override + public void run() { + if (!drawerLayout.isLocked(drawerContainer)) + drawerLayout.closeDrawer(drawerContainer); + onMenuIssue(); + } + }).setExternal(true)); if (Helper.isPlayStoreInstall() && false) extra.add(new NavMenuItem(R.drawable.baseline_bug_report_24, R.string.menu_test, new Runnable() { @@ -543,18 +542,16 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB } })); - if ((getIntentInvite(this).resolveActivity(pm) != null)) - extra.add(new NavMenuItem(R.drawable.baseline_people_24, R.string.menu_invite, new Runnable() { - @Override - public void run() { - if (!drawerLayout.isLocked(drawerContainer)) - drawerLayout.closeDrawer(drawerContainer); - onMenuInvite(); - } - }).setExternal(true)); + extra.add(new NavMenuItem(R.drawable.baseline_people_24, R.string.menu_invite, new Runnable() { + @Override + public void run() { + if (!drawerLayout.isLocked(drawerContainer)) + drawerLayout.closeDrawer(drawerContainer); + onMenuInvite(); + } + }).setExternal(true)); - if ((Helper.isPlayStoreInstall() || BuildConfig.DEBUG) && - Helper.getIntentRate(this).resolveActivity(pm) != null) + if ((Helper.isPlayStoreInstall() || BuildConfig.DEBUG)) extra.add(new NavMenuItem(R.drawable.baseline_star_24, R.string.menu_rate, new Runnable() { @Override public void run() { @@ -564,15 +561,14 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB } }).setExternal(true)); - if (getIntentOtherApps().resolveActivity(pm) != null) - extra.add(new NavMenuItem(R.drawable.baseline_get_app_24, R.string.menu_other, new Runnable() { - @Override - public void run() { - if (!drawerLayout.isLocked(drawerContainer)) - drawerLayout.closeDrawer(drawerContainer); - onMenuOtherApps(); - } - }).setExternal(true)); + extra.add(new NavMenuItem(R.drawable.baseline_get_app_24, R.string.menu_other, new Runnable() { + @Override + public void run() { + if (!drawerLayout.isLocked(drawerContainer)) + drawerLayout.closeDrawer(drawerContainer); + onMenuOtherApps(); + } + }).setExternal(true)); adapterNavMenuExtra.set(extra); @@ -883,12 +879,10 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB .setVisibility(NotificationCompat.VISIBILITY_SECRET); Intent update = new Intent(Intent.ACTION_VIEW, Uri.parse(info.html_url)); - if (update.resolveActivity(getPackageManager()) != null) { - update.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - PendingIntent piUpdate = PendingIntent.getActivity( - ActivityView.this, REQUEST_UPDATE, update, PendingIntent.FLAG_UPDATE_CURRENT); - builder.setContentIntent(piUpdate); - } + update.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent piUpdate = PendingIntent.getActivity( + ActivityView.this, REQUEST_UPDATE, update, PendingIntent.FLAG_UPDATE_CURRENT); + builder.setContentIntent(piUpdate); try { NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); @@ -948,12 +942,19 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) getSupportFragmentManager().popBackStack("unified", 0); - long folder = Long.parseLong(action.split(":", 2)[1]); + String[] parts = action.split(":"); + long folder = Long.parseLong(parts[1]); if (folder > 0) { intent.putExtra("folder", folder); onViewMessages(intent); } + if (parts.length > 2) { + Intent clear = new Intent(this, ServiceUI.class) + .setAction("clear:" + parts[2]); + startService(clear); + } + } else if ("why".equals(action)) { if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) getSupportFragmentManager().popBackStack("unified", 0); @@ -1225,8 +1226,11 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB } private void onViewMessages(Intent intent) { - if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) + if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.STARTED)) { getSupportFragmentManager().popBackStack("messages", FragmentManager.POP_BACK_STACK_INCLUSIVE); + if (content_pane != null) + getSupportFragmentManager().popBackStack("unified", 0); + } Bundle args = new Bundle(); args.putString("type", intent.getStringExtra("type")); diff --git a/app/src/main/java/eu/faircode/email/AdapterAttachment.java b/app/src/main/java/eu/faircode/email/AdapterAttachment.java index a15df904c6..1aa0999556 100644 --- a/app/src/main/java/eu/faircode/email/AdapterAttachment.java +++ b/app/src/main/java/eu/faircode/email/AdapterAttachment.java @@ -19,19 +19,16 @@ package eu.faircode.email; Copyright 2018-2020 by Marcel Bokhorst (M66B) */ -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; -import android.content.pm.PackageManager; -import android.graphics.drawable.Drawable; -import android.net.Uri; import android.os.Bundle; import android.text.TextUtils; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.webkit.MimeTypeMap; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.ProgressBar; @@ -39,7 +36,6 @@ import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; -import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; @@ -51,7 +47,6 @@ import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.ListUpdateCallback; import androidx.recyclerview.widget.RecyclerView; -import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; @@ -115,7 +110,16 @@ public class AdapterAttachment extends RecyclerView.Adapter() { - @Override - protected Drawable onExecute(Context context, Bundle args) throws Throwable { - File file = (File) args.getSerializable("file"); - String type = args.getString("type"); - - Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID, file); - - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setDataAndTypeAndNormalize(uri, type); - - PackageManager pm = context.getPackageManager(); - - ComponentName component = intent.resolveActivity(pm); - if (component == null) - return null; - - return pm.getApplicationIcon(component.getPackageName()); - } - - @Override - protected void onExecuted(Bundle args, Drawable icon) { - long id = args.getLong("id"); - - int pos = getAdapterPosition(); - if (pos == RecyclerView.NO_POSITION) - return; - - EntityAttachment attachment = items.get(pos); - if (attachment == null || !attachment.id.equals(id)) - return; - - if (icon == null) - ivType.setImageResource(R.drawable.baseline_attachment_24); - else - ivType.setImageDrawable(icon); - } - - @Override - protected void onException(Bundle args, Throwable ex) { - Log.unexpectedError(parentFragment.getParentFragmentManager(), ex); - } - }.execute(context, owner, args, "attachment:icon"); } @Override diff --git a/app/src/main/java/eu/faircode/email/AdapterContact.java b/app/src/main/java/eu/faircode/email/AdapterContact.java index 14a60700ca..e2a7724ffc 100644 --- a/app/src/main/java/eu/faircode/email/AdapterContact.java +++ b/app/src/main/java/eu/faircode/email/AdapterContact.java @@ -217,7 +217,7 @@ public class AdapterContact extends RecyclerView.Adapter 0) Collections.sort(parents, parents.get(0).getComparator(context)); - for (TupleFolderEx parent : parents) - if ((show_hidden || !parent.hide) && - (!subscribed_only || - parent.accountProtocol != EntityAccount.TYPE_IMAP || - (parent.subscribed != null && parent.subscribed))) { + for (TupleFolderEx parent : parents) { + if (parent.hide && !show_hidden) + continue; + + List childs = null; + if (parent.child_refs != null) + childs = getHierarchical(parent.child_refs, indentation + 1); + + if (!subscribed_only || + parent.accountProtocol != EntityAccount.TYPE_IMAP || + (parent.subscribed != null && parent.subscribed) || + (childs != null && childs.size() > 0)) { parent.indentation = indentation; result.add(parent); - - if (!parent.collapsed && parent.child_refs != null) - result.addAll(getHierarchical(parent.child_refs, indentation + 1)); + if (!parent.collapsed && childs != null) + result.addAll(childs); } + } return result; } diff --git a/app/src/main/java/eu/faircode/email/AdapterMessage.java b/app/src/main/java/eu/faircode/email/AdapterMessage.java index 94b157164e..82d7778a6e 100644 --- a/app/src/main/java/eu/faircode/email/AdapterMessage.java +++ b/app/src/main/java/eu/faircode/email/AdapterMessage.java @@ -29,6 +29,7 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Person; import android.app.RemoteAction; +import android.content.ActivityNotFoundException; import android.content.ClipData; import android.content.ClipboardManager; import android.content.ContentResolver; @@ -119,6 +120,7 @@ import androidx.core.content.FileProvider; import androidx.core.content.pm.ShortcutInfoCompat; import androidx.core.content.pm.ShortcutManagerCompat; import androidx.core.graphics.ColorUtils; +import androidx.core.util.PatternsCompat; import androidx.fragment.app.Fragment; import androidx.lifecycle.Lifecycle; import androidx.lifecycle.LifecycleObserver; @@ -2920,8 +2922,14 @@ public class AdapterMessage extends RecyclerView.Adapter= Build.VERSION_CODES.O && !BuildConfig.DEBUG) editor.remove("background_service"); - } + } else if (version < 1195) + editor.remove("auto_optimize"); if (version < BuildConfig.VERSION_CODE) editor.putInt("previous_version", version); diff --git a/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java b/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java index fac700957a..1e18f8b008 100644 --- a/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java +++ b/app/src/main/java/eu/faircode/email/BoundaryCallbackMessages.java @@ -71,6 +71,7 @@ import javax.mail.search.OrTerm; import javax.mail.search.ReceivedDateTerm; import javax.mail.search.RecipientStringTerm; import javax.mail.search.SearchTerm; +import javax.mail.search.SizeTerm; import javax.mail.search.SubjectTerm; import io.requery.android.database.sqlite.SQLiteDatabase; @@ -239,6 +240,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback 0 ? " +" : "") + TextUtils.join(",", flags); @@ -736,6 +747,7 @@ public class BoundaryCallbackMessages extends PagedList.BoundaryCallback