From ee2b2e7db7873d99b7b190c54f62b2e6895843b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Prante?= Date: Tue, 30 Jul 2024 17:54:09 +0200 Subject: [PATCH] add net-mail, based on jakarta mail and eclipse angus mail --- gradle/test/junit5.gradle | 1 + net-mail/build.gradle | 14 + .../activation/ActivationDataFlavor.java | 219 + .../java/jakarta/activation/CommandInfo.java | 170 + .../java/jakarta/activation/CommandMap.java | 204 + .../jakarta/activation/CommandObject.java | 38 + .../activation/DataContentHandler.java | 81 + .../activation/DataContentHandlerFactory.java | 31 + .../java/jakarta/activation/DataHandler.java | 857 ++++ .../java/jakarta/activation/DataSource.java | 71 + .../jakarta/activation/FactoryFinder.java | 107 + .../jakarta/activation/FileDataSource.java | 167 + .../java/jakarta/activation/FileTypeMap.java | 106 + .../jakarta/activation/MailcapCommandMap.java | 667 +++ .../jakarta/activation/MailcapRegistry.java | 82 + .../java/jakarta/activation/MimeType.java | 275 ++ .../jakarta/activation/MimeTypeEntry.java | 53 + .../activation/MimeTypeParameterList.java | 323 ++ .../activation/MimeTypeParseException.java | 34 + .../jakarta/activation/MimeTypeRegistry.java | 54 + .../activation/MimetypesFileTypeMap.java | 350 ++ .../jakarta/activation/ServiceLoaderUtil.java | 74 + .../jakarta/activation/URLDataSource.java | 121 + .../UnsupportedDataTypeException.java | 41 + .../main/java/jakarta/activation/Util.java | 78 + .../java/jakarta/activation/package-info.java | 14 + .../spi/MailcapRegistryProvider.java | 56 + .../spi/MimeTypeRegistryProvider.java | 58 + .../jakarta/activation/spi/package-info.java | 18 + .../src/main/java/jakarta/mail/Address.java | 68 + .../mail/AuthenticationFailedException.java | 58 + .../main/java/jakarta/mail/Authenticator.java | 142 + .../src/main/java/jakarta/mail/BodyPart.java | 76 + .../main/java/jakarta/mail/EncodingAware.java | 55 + .../main/java/jakarta/mail/EventQueue.java | 159 + .../main/java/jakarta/mail/FetchProfile.java | 223 + .../src/main/java/jakarta/mail/Flags.java | 667 +++ .../src/main/java/jakarta/mail/Folder.java | 1645 +++++++ .../jakarta/mail/FolderClosedException.java | 81 + .../jakarta/mail/FolderNotFoundException.java | 96 + .../src/main/java/jakarta/mail/Header.java | 91 + .../jakarta/mail/IllegalWriteException.java | 58 + .../main/java/jakarta/mail/MailLogger.java | 432 ++ .../jakarta/mail/MailSessionDefinition.java | 108 + .../jakarta/mail/MailSessionDefinitions.java | 34 + .../src/main/java/jakarta/mail/Message.java | 678 +++ .../main/java/jakarta/mail/MessageAware.java | 36 + .../java/jakarta/mail/MessageContext.java | 100 + .../jakarta/mail/MessageRemovedException.java | 60 + .../java/jakarta/mail/MessagingException.java | 145 + .../mail/MethodNotSupportedException.java | 58 + .../src/main/java/jakarta/mail/Multipart.java | 265 ++ .../jakarta/mail/MultipartDataSource.java | 58 + .../jakarta/mail/NoSuchProviderException.java | 57 + net-mail/src/main/java/jakarta/mail/Part.java | 454 ++ .../jakarta/mail/PasswordAuthentication.java | 59 + .../src/main/java/jakarta/mail/Provider.java | 145 + .../src/main/java/jakarta/mail/Quota.java | 108 + .../java/jakarta/mail/QuotaAwareStore.java | 57 + .../jakarta/mail/ReadOnlyFolderException.java | 80 + .../jakarta/mail/SendFailedException.java | 127 + .../src/main/java/jakarta/mail/Service.java | 632 +++ .../src/main/java/jakarta/mail/Session.java | 1286 +++++ .../src/main/java/jakarta/mail/Store.java | 300 ++ .../jakarta/mail/StoreClosedException.java | 81 + .../src/main/java/jakarta/mail/Transport.java | 397 ++ .../src/main/java/jakarta/mail/UIDFolder.java | 210 + .../src/main/java/jakarta/mail/URLName.java | 774 +++ .../jakarta/mail/event/ConnectionAdapter.java | 47 + .../jakarta/mail/event/ConnectionEvent.java | 79 + .../mail/event/ConnectionListener.java | 48 + .../jakarta/mail/event/FolderAdapter.java | 46 + .../java/jakarta/mail/event/FolderEvent.java | 147 + .../jakarta/mail/event/FolderListener.java | 46 + .../java/jakarta/mail/event/MailEvent.java | 45 + .../mail/event/MessageChangedEvent.java | 88 + .../mail/event/MessageChangedListener.java | 35 + .../mail/event/MessageCountAdapter.java | 42 + .../jakarta/mail/event/MessageCountEvent.java | 138 + .../mail/event/MessageCountListener.java | 39 + .../java/jakarta/mail/event/StoreEvent.java | 96 + .../jakarta/mail/event/StoreListener.java | 35 + .../jakarta/mail/event/TransportAdapter.java | 46 + .../jakarta/mail/event/TransportEvent.java | 164 + .../jakarta/mail/event/TransportListener.java | 52 + .../java/jakarta/mail/event/package-info.java | 22 + .../mail/internet/AddressException.java | 113 + .../mail/internet/ContentDisposition.java | 185 + .../jakarta/mail/internet/ContentType.java | 276 ++ .../mail/internet/HeaderTokenizer.java | 463 ++ .../mail/internet/InternetAddress.java | 1508 ++++++ .../mail/internet/InternetHeaders.java | 690 +++ .../jakarta/mail/internet/MailDateFormat.java | 990 ++++ .../jakarta/mail/internet/MimeBodyPart.java | 1719 +++++++ .../jakarta/mail/internet/MimeMessage.java | 2326 +++++++++ .../jakarta/mail/internet/MimeMultipart.java | 1023 ++++ .../java/jakarta/mail/internet/MimePart.java | 225 + .../mail/internet/MimePartDataSource.java | 156 + .../java/jakarta/mail/internet/MimeUtil.java | 88 + .../jakarta/mail/internet/MimeUtility.java | 1810 +++++++ .../jakarta/mail/internet/NewsAddress.java | 208 + .../jakarta/mail/internet/ParameterList.java | 886 ++++ .../jakarta/mail/internet/ParseException.java | 45 + .../mail/internet/PreencodedMimeBodyPart.java | 105 + .../mail/internet/SharedInputStream.java | 60 + .../jakarta/mail/internet/UniqueValue.java | 94 + .../jakarta/mail/internet/package-info.java | 542 +++ .../main/java/jakarta/mail/package-info.java | 339 ++ .../mail/search/AddressStringTerm.java | 76 + .../java/jakarta/mail/search/AddressTerm.java | 82 + .../java/jakarta/mail/search/AndTerm.java | 113 + .../java/jakarta/mail/search/BodyTerm.java | 103 + .../jakarta/mail/search/ComparisonTerm.java | 86 + .../java/jakarta/mail/search/DateTerm.java | 107 + .../java/jakarta/mail/search/FlagTerm.java | 142 + .../jakarta/mail/search/FromStringTerm.java | 80 + .../java/jakarta/mail/search/FromTerm.java | 73 + .../java/jakarta/mail/search/HeaderTerm.java | 103 + .../mail/search/IntegerComparisonTerm.java | 105 + .../jakarta/mail/search/MessageIDTerm.java | 79 + .../mail/search/MessageNumberTerm.java | 66 + .../java/jakarta/mail/search/NotTerm.java | 77 + .../main/java/jakarta/mail/search/OrTerm.java | 113 + .../jakarta/mail/search/ReceivedDateTerm.java | 72 + .../mail/search/RecipientStringTerm.java | 107 + .../jakarta/mail/search/RecipientTerm.java | 101 + .../jakarta/mail/search/SearchException.java | 45 + .../java/jakarta/mail/search/SearchTerm.java | 56 + .../jakarta/mail/search/SentDateTerm.java | 72 + .../java/jakarta/mail/search/SizeTerm.java | 70 + .../java/jakarta/mail/search/StringTerm.java | 121 + .../java/jakarta/mail/search/SubjectTerm.java | 72 + .../jakarta/mail/search/package-info.java | 37 + .../mail/util/ByteArrayDataSource.java | 185 + .../java/jakarta/mail/util/FactoryFinder.java | 101 + .../jakarta/mail/util/LineInputStream.java | 48 + .../jakarta/mail/util/LineOutputStream.java | 54 + .../mail/util/SharedByteArrayInputStream.java | 94 + .../mail/util/SharedFileInputStream.java | 480 ++ .../jakarta/mail/util/StreamProvider.java | 192 + .../java/jakarta/mail/util/package-info.java | 22 + net-mail/src/main/java/module-info.java | 37 + .../org/xbib/net/mail/dsn/DeliveryStatus.java | 186 + .../net/mail/dsn/DispositionNotification.java | 129 + .../org/xbib/net/mail/dsn/MessageHeaders.java | 93 + .../xbib/net/mail/dsn/MultipartReport.java | 446 ++ .../java/org/xbib/net/mail/dsn/Report.java | 48 + .../net/mail/dsn/message_deliverystatus.java | 116 + .../dsn/message_dispositionnotification.java | 116 + .../xbib/net/mail/dsn/multipart_report.java | 99 + .../org/xbib/net/mail/dsn/package-info.java | 72 + .../xbib/net/mail/dsn/text_rfc822headers.java | 191 + .../xbib/net/mail/handlers/handler_base.java | 92 + .../net/mail/handlers/message_rfc822.java | 105 + .../net/mail/handlers/multipart_mixed.java | 82 + .../xbib/net/mail/handlers/package-info.java | 21 + .../org/xbib/net/mail/handlers/text_html.java | 39 + .../xbib/net/mail/handlers/text_plain.java | 161 + .../org/xbib/net/mail/handlers/text_xml.java | 119 + .../java/org/xbib/net/mail/iap/Argument.java | 440 ++ .../net/mail/iap/BadCommandException.java | 49 + .../java/org/xbib/net/mail/iap/ByteArray.java | 125 + .../net/mail/iap/CommandFailedException.java | 49 + .../net/mail/iap/ConnectionException.java | 56 + .../java/org/xbib/net/mail/iap/Literal.java | 44 + .../xbib/net/mail/iap/LiteralException.java | 34 + .../xbib/net/mail/iap/ParsingException.java | 49 + .../java/org/xbib/net/mail/iap/Protocol.java | 695 +++ .../xbib/net/mail/iap/ProtocolException.java | 71 + .../java/org/xbib/net/mail/iap/Response.java | 606 +++ .../xbib/net/mail/iap/ResponseHandler.java | 27 + .../net/mail/iap/ResponseInputStream.java | 157 + .../org/xbib/net/mail/iap/package-info.java | 21 + .../main/java/org/xbib/net/mail/imap/ACL.java | 82 + .../org/xbib/net/mail/imap/AppendUID.java | 35 + .../java/org/xbib/net/mail/imap/CopyUID.java | 39 + .../org/xbib/net/mail/imap/DefaultFolder.java | 127 + .../org/xbib/net/mail/imap/IMAPBodyPart.java | 467 ++ .../org/xbib/net/mail/imap/IMAPFolder.java | 4166 +++++++++++++++++ .../xbib/net/mail/imap/IMAPInputStream.java | 301 ++ .../org/xbib/net/mail/imap/IMAPMessage.java | 1737 +++++++ .../mail/imap/IMAPMultipartDataSource.java | 63 + .../xbib/net/mail/imap/IMAPNestedMessage.java | 142 + .../org/xbib/net/mail/imap/IMAPProvider.java | 31 + .../xbib/net/mail/imap/IMAPSSLProvider.java | 31 + .../org/xbib/net/mail/imap/IMAPSSLStore.java | 38 + .../org/xbib/net/mail/imap/IMAPStore.java | 2196 +++++++++ .../org/xbib/net/mail/imap/IdleManager.java | 492 ++ .../org/xbib/net/mail/imap/MessageCache.java | 437 ++ .../net/mail/imap/MessageVanishedEvent.java | 60 + .../xbib/net/mail/imap/ModifiedSinceTerm.java | 92 + .../org/xbib/net/mail/imap/OlderTerm.java | 94 + .../xbib/net/mail/imap/ReferralException.java | 63 + .../org/xbib/net/mail/imap/ResyncData.java | 118 + .../java/org/xbib/net/mail/imap/Rights.java | 305 ++ .../java/org/xbib/net/mail/imap/SortTerm.java | 82 + .../java/org/xbib/net/mail/imap/Utility.java | 214 + .../org/xbib/net/mail/imap/YoungerTerm.java | 94 + .../org/xbib/net/mail/imap/package-info.java | 1047 +++++ .../imap/protocol/BASE64MailboxDecoder.java | 177 + .../imap/protocol/BASE64MailboxEncoder.java | 232 + .../org/xbib/net/mail/imap/protocol/BODY.java | 95 + .../net/mail/imap/protocol/BODYSTRUCTURE.java | 454 ++ .../xbib/net/mail/imap/protocol/ENVELOPE.java | 225 + .../xbib/net/mail/imap/protocol/FLAGS.java | 85 + .../net/mail/imap/protocol/FetchItem.java | 56 + .../net/mail/imap/protocol/FetchResponse.java | 334 ++ .../org/xbib/net/mail/imap/protocol/ID.java | 101 + .../net/mail/imap/protocol/IMAPProtocol.java | 3117 ++++++++++++ .../imap/protocol/IMAPReferralException.java | 51 + .../net/mail/imap/protocol/IMAPResponse.java | 146 + .../imap/protocol/IMAPSaslAuthenticator.java | 295 ++ .../net/mail/imap/protocol/INTERNALDATE.java | 126 + .../org/xbib/net/mail/imap/protocol/Item.java | 30 + .../xbib/net/mail/imap/protocol/ListInfo.java | 78 + .../xbib/net/mail/imap/protocol/MODSEQ.java | 53 + .../net/mail/imap/protocol/MailboxInfo.java | 184 + .../net/mail/imap/protocol/MessageSet.java | 121 + .../net/mail/imap/protocol/Namespaces.java | 151 + .../net/mail/imap/protocol/RFC822DATA.java | 76 + .../net/mail/imap/protocol/RFC822SIZE.java | 45 + .../mail/imap/protocol/SaslAuthenticator.java | 29 + .../mail/imap/protocol/SearchSequence.java | 545 +++ .../xbib/net/mail/imap/protocol/Status.java | 144 + .../org/xbib/net/mail/imap/protocol/UID.java | 45 + .../xbib/net/mail/imap/protocol/UIDSet.java | 235 + .../net/mail/imap/protocol/package-info.java | 21 + .../net/mail/mbox/ContentLengthCounter.java | 76 + .../net/mail/mbox/ContentLengthUpdater.java | 119 + .../xbib/net/mail/mbox/DefaultMailbox.java | 112 + .../org/xbib/net/mail/mbox/FileInterface.java | 150 + .../org/xbib/net/mail/mbox/InboxFile.java | 23 + .../org/xbib/net/mail/mbox/LineCounter.java | 66 + .../java/org/xbib/net/mail/mbox/MailFile.java | 29 + .../java/org/xbib/net/mail/mbox/Mailbox.java | 36 + .../org/xbib/net/mail/mbox/MboxFolder.java | 1207 +++++ .../org/xbib/net/mail/mbox/MboxMessage.java | 547 +++ .../org/xbib/net/mail/mbox/MboxProvider.java | 29 + .../org/xbib/net/mail/mbox/MboxStore.java | 117 + .../org/xbib/net/mail/mbox/MessageLoader.java | 289 ++ .../net/mail/mbox/NewlineOutputStream.java | 91 + .../xbib/net/mail/mbox/SolarisMailbox.java | 84 + .../org/xbib/net/mail/mbox/SunOSMailbox.java | 27 + .../org/xbib/net/mail/mbox/SunV3BodyPart.java | 317 ++ .../xbib/net/mail/mbox/SunV3Multipart.java | 206 + .../java/org/xbib/net/mail/mbox/TempFile.java | 174 + .../java/org/xbib/net/mail/mbox/UNIXFile.java | 129 + .../org/xbib/net/mail/mbox/UNIXFolder.java | 76 + .../org/xbib/net/mail/mbox/UNIXInbox.java | 126 + .../java/org/xbib/net/mail/package-info.java | 339 ++ .../org/xbib/net/mail/pop3/AppendStream.java | 67 + .../org/xbib/net/mail/pop3/DefaultFolder.java | 145 + .../org/xbib/net/mail/pop3/POP3Folder.java | 603 +++ .../org/xbib/net/mail/pop3/POP3Message.java | 659 +++ .../org/xbib/net/mail/pop3/POP3Provider.java | 31 + .../xbib/net/mail/pop3/POP3SSLProvider.java | 31 + .../org/xbib/net/mail/pop3/POP3SSLStore.java | 32 + .../org/xbib/net/mail/pop3/POP3Store.java | 534 +++ .../java/org/xbib/net/mail/pop3/Protocol.java | 1246 +++++ .../java/org/xbib/net/mail/pop3/Status.java | 25 + .../java/org/xbib/net/mail/pop3/TempFile.java | 60 + .../net/mail/pop3/WritableSharedFile.java | 84 + .../org/xbib/net/mail/pop3/package-info.java | 821 ++++ .../net/mail/remote/POP3RemoteProvider.java | 29 + .../xbib/net/mail/remote/POP3RemoteStore.java | 38 + .../net/mail/remote/RemoteDefaultFolder.java | 52 + .../org/xbib/net/mail/remote/RemoteInbox.java | 68 + .../org/xbib/net/mail/remote/RemoteStore.java | 137 + .../org/xbib/net/mail/smtp/DigestMD5.java | 237 + .../mail/smtp/SMTPAddressFailedException.java | 85 + .../smtp/SMTPAddressSucceededException.java | 84 + .../org/xbib/net/mail/smtp/SMTPMessage.java | 333 ++ .../xbib/net/mail/smtp/SMTPOutputStream.java | 96 + .../org/xbib/net/mail/smtp/SMTPProvider.java | 31 + .../xbib/net/mail/smtp/SMTPSSLProvider.java | 31 + .../xbib/net/mail/smtp/SMTPSSLTransport.java | 40 + .../net/mail/smtp/SMTPSaslAuthenticator.java | 235 + .../mail/smtp/SMTPSendFailedException.java | 80 + .../mail/smtp/SMTPSenderFailedException.java | 83 + .../org/xbib/net/mail/smtp/SMTPTransport.java | 2784 +++++++++++ .../xbib/net/mail/smtp/SaslAuthenticator.java | 29 + .../org/xbib/net/mail/smtp/package-info.java | 882 ++++ .../org/xbib/net/mail/util/ASCIIUtility.java | 284 ++ .../net/mail/util/BASE64DecoderStream.java | 408 ++ .../net/mail/util/BASE64EncoderStream.java | 293 ++ .../xbib/net/mail/util/BEncoderStream.java | 42 + .../xbib/net/mail/util/CRLFOutputStream.java | 90 + .../xbib/net/mail/util/DecodingException.java | 39 + .../xbib/net/mail/util/DefaultProvider.java | 34 + .../xbib/net/mail/util/LineInputStream.java | 175 + .../xbib/net/mail/util/LineOutputStream.java | 76 + .../xbib/net/mail/util/LogOutputStream.java | 125 + .../net/mail/util/MailConnectException.java | 83 + .../net/mail/util/MailSSLSocketFactory.java | 373 ++ .../net/mail/util/MailStreamProvider.java | 108 + .../java/org/xbib/net/mail/util/PropUtil.java | 150 + .../xbib/net/mail/util/QDecoderStream.java | 72 + .../xbib/net/mail/util/QEncoderStream.java | 82 + .../xbib/net/mail/util/QPDecoderStream.java | 201 + .../xbib/net/mail/util/QPEncoderStream.java | 202 + .../org/xbib/net/mail/util/ReadableMime.java | 39 + .../util/SharedByteArrayOutputStream.java | 39 + .../net/mail/util/SocketConnectException.java | 98 + .../org/xbib/net/mail/util/SocketFetcher.java | 1156 +++++ .../net/mail/util/TimeoutOutputStream.java | 123 + .../xbib/net/mail/util/TraceInputStream.java | 148 + .../xbib/net/mail/util/TraceOutputStream.java | 152 + .../xbib/net/mail/util/UUDecoderStream.java | 338 ++ .../xbib/net/mail/util/UUEncoderStream.java | 210 + .../net/mail/util/WriteTimeoutSocket.java | 371 ++ .../org/xbib/net/mail/util/package-info.java | 49 + .../resources/META-INF/javamail.address.map | 1 + .../resources/META-INF/javamail.charset.map | 78 + .../META-INF/javamail.default.address.map | 1 + .../resources/META-INF/javamail.providers | 4 + net-mail/src/main/resources/META-INF/mailcap | 5 + .../META-INF/services/jakarta.mail.Provider | 4 + .../services/jakarta.mail.util.StreamProvider | 1 + net-mail/src/test/java/module-info.java | 26 + .../net/mail/test/handlers/TextXmlTest.java | 127 + .../xbib/net/mail/test/iap/ProtocolTest.java | 322 ++ .../test/iap/ResponseInputStreamTest.java | 48 + .../xbib/net/mail/test/iap/ResponseTest.java | 166 + .../net/mail/test/imap/IMAPAlertTest.java | 102 + .../net/mail/test/imap/IMAPAuthDebugTest.java | 132 + .../mail/test/imap/IMAPCloseFailureTest.java | 112 + .../test/imap/IMAPConnectFailureTest.java | 135 + .../mail/test/imap/IMAPFetchProfileTest.java | 198 + .../net/mail/test/imap/IMAPFolderTest.java | 531 +++ .../xbib/net/mail/test/imap/IMAPHandler.java | 594 +++ .../xbib/net/mail/test/imap/IMAPIDTest.java | 91 + .../mail/test/imap/IMAPIdleManagerTest.java | 539 +++ .../net/mail/test/imap/IMAPIdleStateTest.java | 130 + .../imap/IMAPIdleUntaggedResponseTest.java | 154 + .../test/imap/IMAPLoginCapabilitiesTest.java | 123 + .../mail/test/imap/IMAPLoginFailureTest.java | 86 + .../net/mail/test/imap/IMAPLoginHandler.java | 62 + .../mail/test/imap/IMAPLoginReferralTest.java | 173 + .../imap/IMAPMessageNumberOutOfRangeTest.java | 110 + .../net/mail/test/imap/IMAPMessageTest.java | 407 ++ .../net/mail/test/imap/IMAPPlainHandler.java | 61 + .../mail/test/imap/IMAPResponseEventTest.java | 129 + .../net/mail/test/imap/IMAPSaslHandler.java | 160 + .../net/mail/test/imap/IMAPSaslLoginTest.java | 75 + .../net/mail/test/imap/IMAPSearchTest.java | 156 + .../net/mail/test/imap/IMAPStoreTest.java | 438 ++ .../mail/test/imap/IMAPUidExpungeTest.java | 462 ++ .../net/mail/test/imap/MessageCacheTest.java | 71 + .../test/imap/protocol/BODYSTRUCTURETest.java | 71 + .../mail/test/imap/protocol/EnvelopeTest.java | 65 + .../test/imap/protocol/IMAPProtocolTest.java | 107 + .../mail/test/imap/protocol/MODSEQTest.java | 54 + .../test/imap/protocol/NamespacesTest.java | 152 + .../mail/test/imap/protocol/StatusTest.java | 94 + .../imap/protocol/StratoImapBugfixTest.java | 70 + .../mail/test/imap/protocol/UIDSetTest.java | 232 + .../net/mail/test/pop3/POP3AuthDebugTest.java | 132 + .../pop3/POP3FolderClosedExceptionTest.java | 171 + .../xbib/net/mail/test/pop3/POP3Handler.java | 289 ++ .../net/mail/test/pop3/POP3MessageTest.java | 125 + .../mail/test/pop3/POP3ReadableMimeTest.java | 136 + .../net/mail/test/pop3/POP3StoreTest.java | 288 ++ .../xbib/net/mail/test/smtp/NopServer.java | 106 + .../net/mail/test/smtp/SMTPAuthDebugTest.java | 132 + .../xbib/net/mail/test/smtp/SMTPBdatTest.java | 153 + .../net/mail/test/smtp/SMTPCloseTest.java | 71 + .../test/smtp/SMTPConnectFailureTest.java | 72 + .../xbib/net/mail/test/smtp/SMTPHandler.java | 284 ++ .../mail/test/smtp/SMTPIOExceptionTest.java | 110 + .../net/mail/test/smtp/SMTPLoginHandler.java | 145 + .../net/mail/test/smtp/SMTPSaslHandler.java | 195 + .../net/mail/test/smtp/SMTPSaslLoginTest.java | 286 ++ .../mail/test/smtp/SMTPUknownCodeTest.java | 83 + .../xbib/net/mail/test/smtp/SMTPUtf8Test.java | 260 + .../mail/test/smtp/SMTPWriteTimeoutTest.java | 97 + .../mail/test/stream/LineInputStreamTest.java | 124 + .../stream/LineInputStreamUtf8FailTest.java | 53 + .../test/stream/LineInputStreamUtf8Test.java | 59 + .../test/test/AsciiStringInputStream.java | 54 + .../net/mail/test/test/NullOutputStream.java | 38 + .../net/mail/test/test/ProtocolHandler.java | 185 + .../net/mail/test/test/ReflectionUtil.java | 54 + .../mail/test/test/SavedSocketFactory.java | 80 + .../xbib/net/mail/test/test/SessionTest.java | 47 + .../mail/test/test/TestSSLSocketFactory.java | 208 + .../xbib/net/mail/test/test/TestServer.java | 268 ++ .../net/mail/test/test/TestSocketFactory.java | 123 + .../mail/test/util/AddAddressHeaderTest.java | 81 + .../xbib/net/mail/test/util/AddFromTest.java | 67 + .../test/util/AllowEncodedMessagesTest.java | 81 + .../xbib/net/mail/test/util/BASE64Test.java | 321 ++ .../test/util/ContentTypeCleanerTest.java | 106 + .../mail/test/util/DecodeParametersTest.java | 44 + .../EncodeFileNameNoEncodeParametersTest.java | 34 + .../mail/test/util/EncodeFileNameTest.java | 67 + .../mail/test/util/GetLocalAddressTest.java | 113 + .../mail/test/util/InternetHeadersTest.java | 112 + .../net/mail/test/util/MimeBodyPartTest.java | 210 + .../net/mail/test/util/MimeMessageTest.java | 255 + .../test/util/MimeMultipartBCSIndexTest.java | 93 + .../test/util/MimeMultipartParseTest.java | 150 + .../test/util/MimeMultipartPreambleTest.java | 118 + .../test/util/MimeMultipartPropertyTest.java | 194 + .../net/mail/test/util/MimeUtilityTest.java | 185 + .../net/mail/test/util/ModifyMessageTest.java | 168 + ...oEncodeFileNameNoEncodeParametersTest.java | 51 + .../mail/test/util/NoEncodeFileNameTest.java | 98 + .../mail/test/util/NonAsciiBoundaryTest.java | 67 + .../mail/test/util/ParameterListDecode.java | 259 + .../test/util/ParametersNoStrictTest.java | 44 + .../xbib/net/mail/test/util/PropUtilTest.java | 208 + .../mail/test/util/QPEncoderStreamTest.java | 45 + .../net/mail/test/util/ReferencesTest.java | 101 + .../mail/test/util/RestrictEncodingTest.java | 109 + .../net/mail/test/util/SocketFetcherTest.java | 919 ++++ .../test/util/TimeoutOutputStreamTest.java | 532 +++ .../mail/test/util/UUDecoderStreamTest.java | 221 + .../net/mail/test/util/Utf8AddressTest.java | 96 + .../test/util/WriteTimeoutSocketTest.java | 536 +++ .../resources/META-INF/javamail.address.map | 1 + .../resources/META-INF/javamail.charset.map | 78 + .../META-INF/javamail.default.address.map | 1 + .../resources/META-INF/javamail.providers | 4 + net-mail/src/test/resources/META-INF/mailcap | 5 + .../src/test/resources/logging.properties | 4 + .../xbib/net/mail/test/imap/protocol/uiddata | 77 + .../org/xbib/net/mail/test/test/keystore.jks | Bin 0 -> 2246 bytes .../org/xbib/net/mail/test/util/paramdata | 139 + .../xbib/net/mail/test/util/paramdatanostrict | 70 + .../org/xbib/net/mail/test/util/uudata | 198 + .../java/org/xbib/net/mime/stream/Base64.java | 6 +- .../src/main/java/module-info.java | 5 + .../java/org/xbib/net/security/auth/MD4.java | 272 ++ .../java/org/xbib/net/security/auth/Ntlm.java | 471 ++ .../net/security/auth/OAuth2SaslClient.java | 116 + .../auth/OAuth2SaslClientFactory.java | 82 + .../xbib/net/security/auth/package-info.java | 21 + settings.gradle | 2 + 438 files changed, 95445 insertions(+), 3 deletions(-) create mode 100644 net-mail/build.gradle create mode 100644 net-mail/src/main/java/jakarta/activation/ActivationDataFlavor.java create mode 100644 net-mail/src/main/java/jakarta/activation/CommandInfo.java create mode 100644 net-mail/src/main/java/jakarta/activation/CommandMap.java create mode 100644 net-mail/src/main/java/jakarta/activation/CommandObject.java create mode 100644 net-mail/src/main/java/jakarta/activation/DataContentHandler.java create mode 100644 net-mail/src/main/java/jakarta/activation/DataContentHandlerFactory.java create mode 100644 net-mail/src/main/java/jakarta/activation/DataHandler.java create mode 100644 net-mail/src/main/java/jakarta/activation/DataSource.java create mode 100644 net-mail/src/main/java/jakarta/activation/FactoryFinder.java create mode 100644 net-mail/src/main/java/jakarta/activation/FileDataSource.java create mode 100644 net-mail/src/main/java/jakarta/activation/FileTypeMap.java create mode 100644 net-mail/src/main/java/jakarta/activation/MailcapCommandMap.java create mode 100644 net-mail/src/main/java/jakarta/activation/MailcapRegistry.java create mode 100644 net-mail/src/main/java/jakarta/activation/MimeType.java create mode 100644 net-mail/src/main/java/jakarta/activation/MimeTypeEntry.java create mode 100644 net-mail/src/main/java/jakarta/activation/MimeTypeParameterList.java create mode 100644 net-mail/src/main/java/jakarta/activation/MimeTypeParseException.java create mode 100644 net-mail/src/main/java/jakarta/activation/MimeTypeRegistry.java create mode 100644 net-mail/src/main/java/jakarta/activation/MimetypesFileTypeMap.java create mode 100644 net-mail/src/main/java/jakarta/activation/ServiceLoaderUtil.java create mode 100644 net-mail/src/main/java/jakarta/activation/URLDataSource.java create mode 100644 net-mail/src/main/java/jakarta/activation/UnsupportedDataTypeException.java create mode 100644 net-mail/src/main/java/jakarta/activation/Util.java create mode 100644 net-mail/src/main/java/jakarta/activation/package-info.java create mode 100644 net-mail/src/main/java/jakarta/activation/spi/MailcapRegistryProvider.java create mode 100644 net-mail/src/main/java/jakarta/activation/spi/MimeTypeRegistryProvider.java create mode 100644 net-mail/src/main/java/jakarta/activation/spi/package-info.java create mode 100644 net-mail/src/main/java/jakarta/mail/Address.java create mode 100644 net-mail/src/main/java/jakarta/mail/AuthenticationFailedException.java create mode 100644 net-mail/src/main/java/jakarta/mail/Authenticator.java create mode 100644 net-mail/src/main/java/jakarta/mail/BodyPart.java create mode 100644 net-mail/src/main/java/jakarta/mail/EncodingAware.java create mode 100644 net-mail/src/main/java/jakarta/mail/EventQueue.java create mode 100644 net-mail/src/main/java/jakarta/mail/FetchProfile.java create mode 100644 net-mail/src/main/java/jakarta/mail/Flags.java create mode 100644 net-mail/src/main/java/jakarta/mail/Folder.java create mode 100644 net-mail/src/main/java/jakarta/mail/FolderClosedException.java create mode 100644 net-mail/src/main/java/jakarta/mail/FolderNotFoundException.java create mode 100644 net-mail/src/main/java/jakarta/mail/Header.java create mode 100644 net-mail/src/main/java/jakarta/mail/IllegalWriteException.java create mode 100644 net-mail/src/main/java/jakarta/mail/MailLogger.java create mode 100644 net-mail/src/main/java/jakarta/mail/MailSessionDefinition.java create mode 100644 net-mail/src/main/java/jakarta/mail/MailSessionDefinitions.java create mode 100644 net-mail/src/main/java/jakarta/mail/Message.java create mode 100644 net-mail/src/main/java/jakarta/mail/MessageAware.java create mode 100644 net-mail/src/main/java/jakarta/mail/MessageContext.java create mode 100644 net-mail/src/main/java/jakarta/mail/MessageRemovedException.java create mode 100644 net-mail/src/main/java/jakarta/mail/MessagingException.java create mode 100644 net-mail/src/main/java/jakarta/mail/MethodNotSupportedException.java create mode 100644 net-mail/src/main/java/jakarta/mail/Multipart.java create mode 100644 net-mail/src/main/java/jakarta/mail/MultipartDataSource.java create mode 100644 net-mail/src/main/java/jakarta/mail/NoSuchProviderException.java create mode 100644 net-mail/src/main/java/jakarta/mail/Part.java create mode 100644 net-mail/src/main/java/jakarta/mail/PasswordAuthentication.java create mode 100644 net-mail/src/main/java/jakarta/mail/Provider.java create mode 100644 net-mail/src/main/java/jakarta/mail/Quota.java create mode 100644 net-mail/src/main/java/jakarta/mail/QuotaAwareStore.java create mode 100644 net-mail/src/main/java/jakarta/mail/ReadOnlyFolderException.java create mode 100644 net-mail/src/main/java/jakarta/mail/SendFailedException.java create mode 100644 net-mail/src/main/java/jakarta/mail/Service.java create mode 100644 net-mail/src/main/java/jakarta/mail/Session.java create mode 100644 net-mail/src/main/java/jakarta/mail/Store.java create mode 100644 net-mail/src/main/java/jakarta/mail/StoreClosedException.java create mode 100644 net-mail/src/main/java/jakarta/mail/Transport.java create mode 100644 net-mail/src/main/java/jakarta/mail/UIDFolder.java create mode 100644 net-mail/src/main/java/jakarta/mail/URLName.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/ConnectionAdapter.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/ConnectionEvent.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/ConnectionListener.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/FolderAdapter.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/FolderEvent.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/FolderListener.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/MailEvent.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/MessageChangedEvent.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/MessageChangedListener.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/MessageCountAdapter.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/MessageCountEvent.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/MessageCountListener.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/StoreEvent.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/StoreListener.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/TransportAdapter.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/TransportEvent.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/TransportListener.java create mode 100644 net-mail/src/main/java/jakarta/mail/event/package-info.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/AddressException.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/ContentDisposition.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/ContentType.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/HeaderTokenizer.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/InternetAddress.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/InternetHeaders.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/MailDateFormat.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/MimeBodyPart.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/MimeMessage.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/MimeMultipart.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/MimePart.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/MimePartDataSource.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/MimeUtil.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/MimeUtility.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/NewsAddress.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/ParameterList.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/ParseException.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/PreencodedMimeBodyPart.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/SharedInputStream.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/UniqueValue.java create mode 100644 net-mail/src/main/java/jakarta/mail/internet/package-info.java create mode 100644 net-mail/src/main/java/jakarta/mail/package-info.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/AddressStringTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/AddressTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/AndTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/BodyTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/ComparisonTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/DateTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/FlagTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/FromStringTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/FromTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/HeaderTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/IntegerComparisonTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/MessageIDTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/MessageNumberTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/NotTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/OrTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/ReceivedDateTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/RecipientStringTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/RecipientTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/SearchException.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/SearchTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/SentDateTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/SizeTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/StringTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/SubjectTerm.java create mode 100644 net-mail/src/main/java/jakarta/mail/search/package-info.java create mode 100644 net-mail/src/main/java/jakarta/mail/util/ByteArrayDataSource.java create mode 100644 net-mail/src/main/java/jakarta/mail/util/FactoryFinder.java create mode 100644 net-mail/src/main/java/jakarta/mail/util/LineInputStream.java create mode 100644 net-mail/src/main/java/jakarta/mail/util/LineOutputStream.java create mode 100644 net-mail/src/main/java/jakarta/mail/util/SharedByteArrayInputStream.java create mode 100644 net-mail/src/main/java/jakarta/mail/util/SharedFileInputStream.java create mode 100644 net-mail/src/main/java/jakarta/mail/util/StreamProvider.java create mode 100644 net-mail/src/main/java/jakarta/mail/util/package-info.java create mode 100644 net-mail/src/main/java/module-info.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/dsn/DeliveryStatus.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/dsn/DispositionNotification.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/dsn/MessageHeaders.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/dsn/MultipartReport.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/dsn/Report.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/dsn/message_deliverystatus.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/dsn/message_dispositionnotification.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/dsn/multipart_report.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/dsn/package-info.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/dsn/text_rfc822headers.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/handlers/handler_base.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/handlers/message_rfc822.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/handlers/multipart_mixed.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/handlers/package-info.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/handlers/text_html.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/handlers/text_plain.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/handlers/text_xml.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/Argument.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/BadCommandException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/ByteArray.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/CommandFailedException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/ConnectionException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/Literal.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/LiteralException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/ParsingException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/Protocol.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/ProtocolException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/Response.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/ResponseHandler.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/ResponseInputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/iap/package-info.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/ACL.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/AppendUID.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/CopyUID.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/DefaultFolder.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/IMAPBodyPart.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/IMAPFolder.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/IMAPInputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/IMAPMessage.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/IMAPMultipartDataSource.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/IMAPNestedMessage.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/IMAPProvider.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/IMAPSSLProvider.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/IMAPSSLStore.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/IMAPStore.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/IdleManager.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/MessageCache.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/MessageVanishedEvent.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/ModifiedSinceTerm.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/OlderTerm.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/ReferralException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/ResyncData.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/Rights.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/SortTerm.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/Utility.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/YoungerTerm.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/package-info.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BASE64MailboxDecoder.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BASE64MailboxEncoder.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BODY.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BODYSTRUCTURE.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ENVELOPE.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FLAGS.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FetchItem.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FetchResponse.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ID.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPProtocol.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPReferralException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPResponse.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPSaslAuthenticator.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/INTERNALDATE.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Item.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ListInfo.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MODSEQ.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MailboxInfo.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MessageSet.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Namespaces.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/RFC822DATA.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/RFC822SIZE.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/SaslAuthenticator.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/SearchSequence.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Status.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/UID.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/UIDSet.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/imap/protocol/package-info.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/ContentLengthCounter.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/ContentLengthUpdater.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/DefaultMailbox.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/FileInterface.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/InboxFile.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/LineCounter.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/MailFile.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/Mailbox.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/MboxFolder.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/MboxMessage.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/MboxProvider.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/MboxStore.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/MessageLoader.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/NewlineOutputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/SolarisMailbox.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/SunOSMailbox.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/SunV3BodyPart.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/SunV3Multipart.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/TempFile.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXFile.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXFolder.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXInbox.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/package-info.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/AppendStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/DefaultFolder.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Folder.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Message.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Provider.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/POP3SSLProvider.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/POP3SSLStore.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Store.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/Protocol.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/Status.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/TempFile.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/WritableSharedFile.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/pop3/package-info.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/remote/POP3RemoteProvider.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/remote/POP3RemoteStore.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/remote/RemoteDefaultFolder.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/remote/RemoteInbox.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/remote/RemoteStore.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/DigestMD5.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPAddressFailedException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPAddressSucceededException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPMessage.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPOutputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPProvider.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSSLProvider.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSSLTransport.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSaslAuthenticator.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSendFailedException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSenderFailedException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPTransport.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/SaslAuthenticator.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/smtp/package-info.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/ASCIIUtility.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/BASE64DecoderStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/BASE64EncoderStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/BEncoderStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/CRLFOutputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/DecodingException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/DefaultProvider.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/LineInputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/LineOutputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/LogOutputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/MailConnectException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/MailSSLSocketFactory.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/MailStreamProvider.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/PropUtil.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/QDecoderStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/QEncoderStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/QPDecoderStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/QPEncoderStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/ReadableMime.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/SharedByteArrayOutputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/SocketConnectException.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/SocketFetcher.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/TimeoutOutputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/TraceInputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/TraceOutputStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/UUDecoderStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/UUEncoderStream.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/WriteTimeoutSocket.java create mode 100644 net-mail/src/main/java/org/xbib/net/mail/util/package-info.java create mode 100644 net-mail/src/main/resources/META-INF/javamail.address.map create mode 100644 net-mail/src/main/resources/META-INF/javamail.charset.map create mode 100644 net-mail/src/main/resources/META-INF/javamail.default.address.map create mode 100644 net-mail/src/main/resources/META-INF/javamail.providers create mode 100644 net-mail/src/main/resources/META-INF/mailcap create mode 100644 net-mail/src/main/resources/META-INF/services/jakarta.mail.Provider create mode 100644 net-mail/src/main/resources/META-INF/services/jakarta.mail.util.StreamProvider create mode 100644 net-mail/src/test/java/module-info.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/handlers/TextXmlTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/iap/ProtocolTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/iap/ResponseInputStreamTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/iap/ResponseTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPAlertTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPAuthDebugTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPCloseFailureTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPConnectFailureTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPFetchProfileTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPFolderTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPHandler.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIDTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleManagerTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleStateTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleUntaggedResponseTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginCapabilitiesTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginFailureTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginHandler.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginReferralTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPMessageNumberOutOfRangeTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPMessageTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPPlainHandler.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPResponseEventTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSaslHandler.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSaslLoginTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSearchTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPStoreTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPUidExpungeTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/MessageCacheTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/BODYSTRUCTURETest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/EnvelopeTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/IMAPProtocolTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/MODSEQTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/NamespacesTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/StatusTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/StratoImapBugfixTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/UIDSetTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3AuthDebugTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3FolderClosedExceptionTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3Handler.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3MessageTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3ReadableMimeTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3StoreTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/NopServer.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPAuthDebugTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPBdatTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPCloseTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPConnectFailureTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPHandler.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPIOExceptionTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPLoginHandler.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPSaslHandler.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPSaslLoginTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPUknownCodeTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPUtf8Test.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPWriteTimeoutTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamUtf8FailTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamUtf8Test.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/test/AsciiStringInputStream.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/test/NullOutputStream.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/test/ProtocolHandler.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/test/ReflectionUtil.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/test/SavedSocketFactory.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/test/SessionTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/test/TestSSLSocketFactory.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/test/TestServer.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/test/TestSocketFactory.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/AddAddressHeaderTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/AddFromTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/AllowEncodedMessagesTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/BASE64Test.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/ContentTypeCleanerTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/DecodeParametersTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/EncodeFileNameNoEncodeParametersTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/EncodeFileNameTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/GetLocalAddressTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/InternetHeadersTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/MimeBodyPartTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMessageTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartBCSIndexTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartParseTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartPreambleTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartPropertyTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/MimeUtilityTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/ModifyMessageTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/NoEncodeFileNameNoEncodeParametersTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/NoEncodeFileNameTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/NonAsciiBoundaryTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/ParameterListDecode.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/ParametersNoStrictTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/PropUtilTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/QPEncoderStreamTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/ReferencesTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/RestrictEncodingTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/SocketFetcherTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/TimeoutOutputStreamTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/UUDecoderStreamTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/Utf8AddressTest.java create mode 100644 net-mail/src/test/java/org/xbib/net/mail/test/util/WriteTimeoutSocketTest.java create mode 100644 net-mail/src/test/resources/META-INF/javamail.address.map create mode 100644 net-mail/src/test/resources/META-INF/javamail.charset.map create mode 100644 net-mail/src/test/resources/META-INF/javamail.default.address.map create mode 100644 net-mail/src/test/resources/META-INF/javamail.providers create mode 100644 net-mail/src/test/resources/META-INF/mailcap create mode 100644 net-mail/src/test/resources/logging.properties create mode 100644 net-mail/src/test/resources/org/xbib/net/mail/test/imap/protocol/uiddata create mode 100644 net-mail/src/test/resources/org/xbib/net/mail/test/test/keystore.jks create mode 100644 net-mail/src/test/resources/org/xbib/net/mail/test/util/paramdata create mode 100644 net-mail/src/test/resources/org/xbib/net/mail/test/util/paramdatanostrict create mode 100644 net-mail/src/test/resources/org/xbib/net/mail/test/util/uudata create mode 100644 net-security-auth/src/main/java/module-info.java create mode 100644 net-security-auth/src/main/java/org/xbib/net/security/auth/MD4.java create mode 100644 net-security-auth/src/main/java/org/xbib/net/security/auth/Ntlm.java create mode 100644 net-security-auth/src/main/java/org/xbib/net/security/auth/OAuth2SaslClient.java create mode 100644 net-security-auth/src/main/java/org/xbib/net/security/auth/OAuth2SaslClientFactory.java create mode 100644 net-security-auth/src/main/java/org/xbib/net/security/auth/package-info.java diff --git a/gradle/test/junit5.gradle b/gradle/test/junit5.gradle index 2b98eff..cbfd0c4 100644 --- a/gradle/test/junit5.gradle +++ b/gradle/test/junit5.gradle @@ -1,5 +1,6 @@ dependencies { testImplementation testLibs.junit.jupiter.api + testImplementation testLibs.junit.jupiter.params testImplementation testLibs.hamcrest testRuntimeOnly testLibs.junit.jupiter.engine testRuntimeOnly testLibs.junit.jupiter.platform.launcher diff --git a/net-mail/build.gradle b/net-mail/build.gradle new file mode 100644 index 0000000..d9584ab --- /dev/null +++ b/net-mail/build.gradle @@ -0,0 +1,14 @@ +dependencies { + api project(':net-security-auth') +} + +def moduleName = 'org.xbib.net.mail.test' +def patchArgs = ['--patch-module', "$moduleName=" + files(sourceSets.test.resources.srcDirs).asPath ] + +tasks.named('compileTestJava') { + options.compilerArgs += patchArgs +} + +tasks.named('test') { + jvmArgs += patchArgs +} diff --git a/net-mail/src/main/java/jakarta/activation/ActivationDataFlavor.java b/net-mail/src/main/java/jakarta/activation/ActivationDataFlavor.java new file mode 100644 index 0000000..fd548c7 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/ActivationDataFlavor.java @@ -0,0 +1,219 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +/** + * The ActivationDataFlavor class is similar to the JDK's + * java.awt.datatransfer.DataFlavor class. It allows + * Jakarta Activation to + * set all three values stored by the DataFlavor class via a new + * constructor. It also contains improved MIME parsing in the equals + * method. Except for the improved parsing, its semantics are + * identical to that of the JDK's DataFlavor class. + */ + +public class ActivationDataFlavor { + + /* + * Raison d'etre: + * + * The DataFlavor class included in JDK 1.1 has several limitations + * including poor MIME type parsing, and the limitation of + * only supporting serialized objects and InputStreams as + * representation objects. This class 'fixes' that. + */ + + private final String mimeType; + private MimeType mimeObject = null; + private String humanPresentableName; + private Class representationClass = null; + + /** + * Construct an ActivationDataFlavor that represents an arbitrary + * Java object. + *

+ * The returned ActivationDataFlavor will have the following + * characteristics: + *

+ * representationClass = representationClass + * mimeType = mimeType + * humanName = humanName + * + * @param representationClass the class used in this ActivationDataFlavor + * @param mimeType the MIME type of the data represented by this class + * @param humanPresentableName the human presentable name of the flavor + */ + public ActivationDataFlavor(Class representationClass, + String mimeType, String humanPresentableName) { + this.mimeType = mimeType; + this.humanPresentableName = humanPresentableName; + this.representationClass = representationClass; + } + + /** + * Construct an ActivationDataFlavor that represents a MimeType. + *

+ * The returned ActivationDataFlavor will have the following + * characteristics: + *

+ * If the mimeType is "application/x-java-serialized-object; + * class=", the result is the same as calling new + * ActivationDataFlavor(Class.forName()) as above. + *

+ * otherwise: + *

+ * representationClass = InputStream

+ * mimeType = mimeType + * + * @param representationClass the class used in this ActivationDataFlavor + * @param humanPresentableName the human presentable name of the flavor + */ + public ActivationDataFlavor(Class representationClass, + String humanPresentableName) { + this.mimeType = "application/x-java-serialized-object"; + this.representationClass = representationClass; + this.humanPresentableName = humanPresentableName; + } + + /** + * Construct an ActivationDataFlavor that represents a MimeType. + *

+ * The returned ActivationDataFlavor will have the following + * characteristics: + *

+ * If the mimeType is "application/x-java-serialized-object; class=", + * the result is the same as calling new + * ActivationDataFlavor(Class.forName()) as above, otherwise: + *

+ * representationClass = InputStream
+ * mimeType = mimeType + * + * @param mimeType the MIME type of the data represented by this class + * @param humanPresentableName the human presentable name of the flavor + */ + public ActivationDataFlavor(String mimeType, String humanPresentableName) { + this.mimeType = mimeType; + try { + this.representationClass = Class.forName("java.io.InputStream"); + } catch (ClassNotFoundException ex) { + // XXX - should never happen, ignore it + } + this.humanPresentableName = humanPresentableName; + } + + /** + * Return the MIME type for this ActivationDataFlavor. + * + * @return the MIME type + */ + public String getMimeType() { + return mimeType; + } + + /** + * Return the representation class. + * + * @return the representation class + */ + public Class getRepresentationClass() { + return representationClass; + } + + /** + * Return the Human Presentable name. + * + * @return the human presentable name + */ + public String getHumanPresentableName() { + return humanPresentableName; + } + + /** + * Set the human presentable name. + * + * @param humanPresentableName the name to set + */ + public void setHumanPresentableName(String humanPresentableName) { + this.humanPresentableName = humanPresentableName; + } + + /** + * Compares the ActivationDataFlavor passed in with this + * ActivationDataFlavor; calls the isMimeTypeEqual method. + * + * @param dataFlavor the ActivationDataFlavor to compare with + * @return true if the MIME type and representation class + * are the same + */ + public boolean equals(ActivationDataFlavor dataFlavor) { + return (isMimeTypeEqual(dataFlavor.mimeType) && + dataFlavor.getRepresentationClass() == representationClass); + } + + /** + * @param o the Object to compare with + * @return true if the object is also an ActivationDataFlavor + * and is equal to this + */ + @Override + public boolean equals(Object o) { + return ((o instanceof ActivationDataFlavor) && + equals((ActivationDataFlavor) o)); + } + + /** + * Returns hash code for this ActivationDataFlavor. + * For two equal ActivationDataFlavors, hash codes are equal. + * For the String + * that matches ActivationDataFlavor.equals(String), it is not + * guaranteed that ActivationDataFlavor's hash code is equal + * to the hash code of the String. + * + * @return a hash code for this ActivationDataFlavor + */ + public int hashCode() { + int total = 0; + + if (representationClass != null) { + total += representationClass.hashCode(); + } + + // XXX - MIME type equality is too complicated so we don't + // include it in the hashCode + + return total; + } + + /** + * Is the string representation of the MIME type passed in equivalent + * to the MIME type of this ActivationDataFlavor.

+ *

+ * ActivationDataFlavor delegates the comparison of MIME types to + * the MimeType class included as part of Jakarta Activation. + * + * @param mimeType the MIME type + * @return true if the same MIME type + */ + public boolean isMimeTypeEqual(String mimeType) { + MimeType mt = null; + try { + if (mimeObject == null) + mimeObject = new MimeType(this.mimeType); + mt = new MimeType(mimeType); + } catch (MimeTypeParseException e) { + // something didn't parse, do a crude comparison + return this.mimeType.equalsIgnoreCase(mimeType); + } + + return mimeObject.match(mt); + } + +} diff --git a/net-mail/src/main/java/jakarta/activation/CommandInfo.java b/net-mail/src/main/java/jakarta/activation/CommandInfo.java new file mode 100644 index 0000000..8b58daf --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/CommandInfo.java @@ -0,0 +1,170 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * The CommandInfo class is used by CommandMap implementations to + * describe the results of command requests. It provides the requestor + * with both the verb requested, as well as an instance of the + * bean. There is also a method that will return the name of the + * class that implements the command but it is not guaranteed to + * return a valid value. The reason for this is to allow CommandMap + * implmentations that subclass CommandInfo to provide special + * behavior. For example a CommandMap could dynamically generate + * JavaBeans. In this case, it might not be possible to create an + * object with all the correct state information solely from the class + * name. + */ + +public class CommandInfo { + private String verb; + private String className; + + /** + * The Constructor for CommandInfo. + * + * @param verb The command verb this CommandInfo decribes. + * @param className The command's fully qualified class name. + */ + public CommandInfo(String verb, String className) { + this.verb = verb; + this.className = className; + } + + /** + * Return the command verb. + * + * @return the command verb. + */ + public String getCommandName() { + return verb; + } + + /** + * Return the command's class name. This method MAY return null in + * cases where a CommandMap subclassed CommandInfo for its + * own purposes. In other words, it might not be possible to + * create the correct state in the command by merely knowing + * its class name. DO NOT DEPEND ON THIS METHOD RETURNING + * A VALID VALUE! + * + * @return The class name of the command, or null + */ + public String getCommandClass() { + return className; + } + + /** + * Return the instantiated JavaBean component. + *

+ * If the current runtime environment supports + * Beans.instantiate, + * use it to instantiate the JavaBeans component. Otherwise, use + * {@link Class#forName(String)} Class.forName}. + *

+ * The component class needs to be public. + * On Java SE 9 and newer, if the component class is in a named module, + * it needs to be in an exported package. + *

+ * If the bean implements the jakarta.activation.CommandObject + * interface, call its setCommandContext method. + *

+ * If the DataHandler parameter is null, then the bean is + * instantiated with no data. NOTE: this may be useful + * if for some reason the DataHandler that is passed in + * throws IOExceptions when this method attempts to + * access its InputStream. It will allow the caller to + * retrieve a reference to the bean if it can be + * instantiated. + *

+ * If the bean does NOT implement the CommandObject interface, + * this method will check if it implements the + * java.io.Externalizable interface. If it does, the bean's + * readExternal method will be called if an InputStream + * can be acquired from the DataHandler. + * + * @param dh The DataHandler that describes the data to be + * passed to the command. + * @param loader The ClassLoader to be used to instantiate the bean. + * @return The bean + * @throws IOException for failures reading data + * @throws ClassNotFoundException if command object class can't + * be found + * @see CommandObject + */ + public Object getCommandObject(DataHandler dh, ClassLoader loader) + throws IOException, ClassNotFoundException { + Object new_bean = null; + + // try to instantiate the bean + new_bean = Beans.instantiate(loader, className); + + // if we got one and it is a CommandObject + if (new_bean != null) { + if (new_bean instanceof CommandObject) { + ((CommandObject) new_bean).setCommandContext(verb, dh); + } + } + return new_bean; + } + + /** + * Helper class to invoke Beans.instantiate reflectively or the equivalent + * with core reflection when module java.desktop is not readable. + */ + private static final class Beans { + static final Method instantiateMethod; + + static { + Method m; + try { + Class c = Class.forName("java.beans.Beans"); + m = c.getDeclaredMethod("instantiate", ClassLoader.class, String.class); + } catch (ClassNotFoundException | NoSuchMethodException e) { + m = null; + } + instantiateMethod = m; + } + + /** + * Equivalent to invoking java.beans.Beans.instantiate(loader, cn) + */ + static Object instantiate(ClassLoader loader, String cn) + throws ClassNotFoundException { + if (instantiateMethod != null) { + // invoke Beans.instantiate + try { + return instantiateMethod.invoke(null, loader, cn); + } catch (InvocationTargetException | IllegalAccessException e) { + // + } + + } else { + + if (loader == null) { + loader = ClassLoader.getSystemClassLoader(); + } + Class beanClass = Class.forName(cn, true, loader); + try { + return beanClass.getDeclaredConstructor().newInstance(); + } catch (Exception ex) { + throw new ClassNotFoundException(beanClass + ": " + ex, ex); + } + + } + return null; + } + } +} diff --git a/net-mail/src/main/java/jakarta/activation/CommandMap.java b/net-mail/src/main/java/jakarta/activation/CommandMap.java new file mode 100644 index 0000000..579f483 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/CommandMap.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +import java.util.Map; +import java.util.WeakHashMap; + + +/** + * The CommandMap class provides an interface to a registry of + * command objects available in the system. + * Developers are expected to either use the CommandMap + * implementation included with this package (MailcapCommandMap) or + * develop their own. Note that some of the methods in this class are + * abstract. + */ +public abstract class CommandMap { + private static CommandMap defaultCommandMap = null; + private static Map map = new WeakHashMap<>(); + + /** + * Default (empty) constructor. + */ + protected CommandMap() { + } + + /** + * Get the default CommandMap. + * + *

+ * + * @return the CommandMap + */ + public static synchronized CommandMap getDefaultCommandMap() { + if (defaultCommandMap != null) + return defaultCommandMap; + + // fetch per-thread-context-class-loader default + ClassLoader tccl = Util.getContextClassLoader(); + CommandMap def = map.get(tccl); + if (def == null) { + def = new MailcapCommandMap(); + map.put(tccl, def); + } + return def; + } + + /** + * Set the default CommandMap. Reset the CommandMap to the default by + * calling this method with null. + * + * @param commandMap The new default CommandMap. + */ + public static synchronized void setDefaultCommandMap(CommandMap commandMap) { + map.remove(Util.getContextClassLoader()); + defaultCommandMap = commandMap; + } + + /** + * Get the preferred command list from a MIME Type. The actual semantics + * are determined by the implementation of the CommandMap. + * + * @param mimeType the MIME type + * @return the CommandInfo classes that represent the command Beans. + */ + abstract public CommandInfo[] getPreferredCommands(String mimeType); + + /** + * Get the preferred command list from a MIME Type. The actual semantics + * are determined by the implementation of the CommandMap.

+ *

+ * The DataSource provides extra information, such as + * the file name, that a CommandMap implementation may use to further + * refine the list of commands that are returned. The implementation + * in this class simply calls the getPreferredCommands + * method that ignores this argument. + * + * @param mimeType the MIME type + * @param ds a DataSource for the data + * @return the CommandInfo classes that represent the command Beans. + * @since JAF 1.1 + */ + public CommandInfo[] getPreferredCommands(String mimeType, DataSource ds) { + return getPreferredCommands(mimeType); + } + + /** + * Get all the available commands for this type. This method + * should return all the possible commands for this MIME type. + * + * @param mimeType the MIME type + * @return the CommandInfo objects representing all the commands. + */ + abstract public CommandInfo[] getAllCommands(String mimeType); + + /** + * Get all the available commands for this type. This method + * should return all the possible commands for this MIME type.

+ *

+ * The DataSource provides extra information, such as + * the file name, that a CommandMap implementation may use to further + * refine the list of commands that are returned. The implementation + * in this class simply calls the getAllCommands + * method that ignores this argument. + * + * @param mimeType the MIME type + * @param ds a DataSource for the data + * @return the CommandInfo objects representing all the commands. + * @since JAF 1.1 + */ + public CommandInfo[] getAllCommands(String mimeType, DataSource ds) { + return getAllCommands(mimeType); + } + + /** + * Get the default command corresponding to the MIME type. + * + * @param mimeType the MIME type + * @param cmdName the command name + * @return the CommandInfo corresponding to the command. + */ + abstract public CommandInfo getCommand(String mimeType, String cmdName); + + /** + * Get the default command corresponding to the MIME type.

+ *

+ * The DataSource provides extra information, such as + * the file name, that a CommandMap implementation may use to further + * refine the command that is chosen. The implementation + * in this class simply calls the getCommand + * method that ignores this argument. + * + * @param mimeType the MIME type + * @param cmdName the command name + * @param ds a DataSource for the data + * @return the CommandInfo corresponding to the command. + * @since JAF 1.1 + */ + public CommandInfo getCommand(String mimeType, String cmdName, + DataSource ds) { + return getCommand(mimeType, cmdName); + } + + /** + * Locate a DataContentHandler that corresponds to the MIME type. + * The mechanism and semantics for determining this are determined + * by the implementation of the particular CommandMap. + * + * @param mimeType the MIME type + * @return the DataContentHandler for the MIME type + */ + abstract public DataContentHandler createDataContentHandler(String + mimeType); + + /** + * Locate a DataContentHandler that corresponds to the MIME type. + * The mechanism and semantics for determining this are determined + * by the implementation of the particular CommandMap.

+ *

+ * The DataSource provides extra information, such as + * the file name, that a CommandMap implementation may use to further + * refine the choice of DataContentHandler. The implementation + * in this class simply calls the createDataContentHandler + * method that ignores this argument. + * + * @param mimeType the MIME type + * @param ds a DataSource for the data + * @return the DataContentHandler for the MIME type + * @since JAF 1.1 + */ + public DataContentHandler createDataContentHandler(String mimeType, + DataSource ds) { + return createDataContentHandler(mimeType); + } + + /** + * Get all the MIME types known to this command map. + * If the command map doesn't support this operation, + * null is returned. + * + * @return array of MIME types as strings, or null if not supported + * @since JAF 1.1 + */ + public String[] getMimeTypes() { + return null; + } +} diff --git a/net-mail/src/main/java/jakarta/activation/CommandObject.java b/net-mail/src/main/java/jakarta/activation/CommandObject.java new file mode 100644 index 0000000..88cdfa1 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/CommandObject.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +import java.io.IOException; + +/** + * JavaBeans components that are Jakarta Activation aware implement + * this interface to find out which command verb they're being asked + * to perform, and to obtain the DataHandler representing the + * data they should operate on. JavaBeans that don't implement + * this interface may be used as well. Such commands may obtain + * the data using the Externalizable interface, or using an + * application-specific method. + */ +public interface CommandObject { + + /** + * Initialize the Command with the verb it is requested to handle + * and the DataHandler that describes the data it will + * operate on. NOTE: it is acceptable for the caller + * to pass null as the value for DataHandler. + * + * @param verb The Command Verb this object refers to. + * @param dh The DataHandler. + * @throws IOException for failures accessing data + */ + void setCommandContext(String verb, DataHandler dh) + throws IOException; +} diff --git a/net-mail/src/main/java/jakarta/activation/DataContentHandler.java b/net-mail/src/main/java/jakarta/activation/DataContentHandler.java new file mode 100644 index 0000000..f0d163b --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/DataContentHandler.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * The DataContentHandler interface is implemented by objects that can + * be used to extend the capabilities of the DataHandler's implementation + * of the Transferable interface. Through DataContentHandlers + * the framework can be extended to convert streams in to objects, and + * to write objects to streams.

+ *

+ * Applications don't generally call the methods in DataContentHandlers + * directly. Instead, an application calls the equivalent methods in + * DataHandler. The DataHandler will attempt to find an appropriate + * DataContentHandler that corresponds to its MIME type using the + * current DataContentHandlerFactory. The DataHandler then calls + * through to the methods in the DataContentHandler. + */ + +public interface DataContentHandler { + /** + * Returns an array of ActivationDataFlavor objects indicating the flavors + * the data can be provided in. The array should be ordered according to + * preference for providing the data (from most richly descriptive to + * least descriptive). + * + * @return The ActivationDataFlavors. + */ + ActivationDataFlavor[] getTransferDataFlavors(); + + /** + * Returns an object which represents the data to be transferred. + * The class of the object returned is defined by the representation class + * of the flavor. + * + * @param df The ActivationDataFlavor representing the requested type. + * @param ds The DataSource representing the data to be converted. + * @return The constructed Object. + * @throws IOException if the handler doesn't + * support the requested flavor + * @throws IOException if the data can't be accessed + */ + Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws IOException; + + /** + * Return an object representing the data in its most preferred form. + * Generally this will be the form described by the first + * ActivationDataFlavor returned by the + * getTransferDataFlavors method. + * + * @param ds The DataSource representing the data to be converted. + * @return The constructed Object. + * @throws IOException if the data can't be accessed + */ + Object getContent(DataSource ds) throws IOException; + + /** + * Convert the object to a byte stream of the specified MIME type + * and write it to the output stream. + * + * @param obj The object to be converted. + * @param mimeType The requested MIME type of the resulting byte stream. + * @param os The output stream into which to write the converted + * byte stream. + * @throws IOException errors writing to the stream + */ + void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException; +} diff --git a/net-mail/src/main/java/jakarta/activation/DataContentHandlerFactory.java b/net-mail/src/main/java/jakarta/activation/DataContentHandlerFactory.java new file mode 100644 index 0000000..8eb0dcc --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/DataContentHandlerFactory.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +/** + * This interface defines a factory for DataContentHandlers. An + * implementation of this interface should map a MIME type into an + * instance of DataContentHandler. The design pattern for classes implementing + * this interface is the same as for the ContentHandler mechanism used in + * java.net.URL. + */ + +public interface DataContentHandlerFactory { + + /** + * Creates a new DataContentHandler object for the MIME type. + * + * @param mimeType the MIME type to create the DataContentHandler for. + * @return The new DataContentHandler, or null + * if none are found. + */ + DataContentHandler createDataContentHandler(String mimeType); +} diff --git a/net-mail/src/main/java/jakarta/activation/DataHandler.java b/net-mail/src/main/java/jakarta/activation/DataHandler.java new file mode 100644 index 0000000..ab60b8c --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/DataHandler.java @@ -0,0 +1,857 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PipedInputStream; +import java.io.PipedOutputStream; +import java.net.URL; +import java.nio.charset.Charset; + +/** + * The DataHandler class provides a consistent interface to data + * available in many different sources and formats. + * It manages simple stream to string conversions and related operations + * using DataContentHandlers. + * It provides access to commands that can operate on the data. + * The commands are found using a CommandMap.

+ * + * DataHandler and CommandMaps

+ * The DataHandler keeps track of the current CommandMap that it uses to + * service requests for commands (getCommand, + * getAllCommands, getPreferredCommands). + * Each instance of a DataHandler may have a CommandMap associated with + * it using the setCommandMap method. If a CommandMap was + * not set, DataHandler calls the getDefaultCommandMap + * method in CommandMap and uses the value it returns. See + * CommandMap for more information.

+ * + * DataHandler and URLs

+ * The current DataHandler implementation creates a private + * instance of URLDataSource when it is constructed with a URL. + * + * @see CommandMap + * @see DataContentHandler + * @see DataSource + * @see URLDataSource + */ + +public class DataHandler { + + // our transfer flavors + private static final ActivationDataFlavor[] emptyFlavors = + new ActivationDataFlavor[0]; + // our DataContentHandlerFactory + private static DataContentHandlerFactory factory = null; + // Use the datasource to indicate whether we were started via the + // DataSource constructor or the object constructor. + private DataSource dataSource = null; + private DataSource objDataSource = null; + // The Object and mimetype from the constructor (if passed in). + // object remains null if it was instantiated with a + // DataSource. + private Object object = null; + private String objectMimeType = null; + // Keep track of the CommandMap + private CommandMap currentCommandMap = null; + private ActivationDataFlavor[] transferFlavors = emptyFlavors; + // our DataContentHandler + private DataContentHandler dataContentHandler = null; + private DataContentHandler factoryDCH = null; + private DataContentHandlerFactory oldFactory = null; + // the short representation of the ContentType (sans params) + private String shortType = null; + + /** + * Create a DataHandler instance referencing the + * specified DataSource. The data exists in a byte stream form. + * The DataSource will provide an InputStream to access the data. + * + * @param ds the DataSource + */ + public DataHandler(DataSource ds) { + // save a reference to the incoming DS + dataSource = ds; + oldFactory = factory; // keep track of the factory + } + + /** + * Create a DataHandler instance representing an object + * of this MIME type. This constructor is + * used when the application already has an in-memory representation + * of the data in the form of a Java Object. + * + * @param obj the Java Object + * @param mimeType the MIME type of the object + */ + public DataHandler(Object obj, String mimeType) { + object = obj; + objectMimeType = mimeType; + oldFactory = factory; // keep track of the factory + } + + /** + * Create a DataHandler instance referencing a URL. + * The DataHandler internally creates a URLDataSource + * instance to represent the URL. + * + * @param url a URL object + */ + public DataHandler(URL url) { + dataSource = new URLDataSource(url); + oldFactory = factory; // keep track of the factory + } + + /** + * Sets the DataContentHandlerFactory. The DataContentHandlerFactory + * is called first to find DataContentHandlers. + * The DataContentHandlerFactory can only be set once. + *

+ * If the DataContentHandlerFactory has already been set, + * this method throws an Error. + * + * @param newFactory the DataContentHandlerFactory + * @throws Error if the factory has already been defined. + * @see DataContentHandlerFactory + */ + public static synchronized void setDataContentHandlerFactory( + DataContentHandlerFactory newFactory) { + if (factory != null) + throw new Error("DataContentHandlerFactory already defined"); + factory = newFactory; + } + + /** + * Return the CommandMap for this instance of DataHandler. + */ + private synchronized CommandMap getCommandMap() { + if (currentCommandMap != null) + return currentCommandMap; + else + return CommandMap.getDefaultCommandMap(); + } + + /** + * Set the CommandMap for use by this DataHandler. + * Setting it to null causes the CommandMap to revert + * to the CommandMap returned by the + * CommandMap.getDefaultCommandMap method. + * Changing the CommandMap, or setting it to null, + * clears out any data cached from the previous CommandMap. + * + * @param commandMap the CommandMap to use in this DataHandler + * @see CommandMap#setDefaultCommandMap + */ + public synchronized void setCommandMap(CommandMap commandMap) { + if (commandMap != currentCommandMap || commandMap == null) { + // clear cached values... + transferFlavors = emptyFlavors; + dataContentHandler = null; + + currentCommandMap = commandMap; + } + } + + /** + * Return the DataSource associated with this instance + * of DataHandler. + *

+ * For DataHandlers that have been instantiated with a DataSource, + * this method returns the DataSource that was used to create the + * DataHandler object. In other cases the DataHandler + * constructs a DataSource from the data used to construct + * the DataHandler. DataSources created for DataHandlers not + * instantiated with a DataSource are cached for performance + * reasons. + * + * @return a valid DataSource object for this DataHandler + */ + public DataSource getDataSource() { + if (dataSource == null) { + // create one on the fly + if (objDataSource == null) + objDataSource = new DataHandlerDataSource(this); + return objDataSource; + } + return dataSource; + } + + /** + * Return the name of the data object. If this DataHandler + * was created with a DataSource, this method calls through + * to the DataSource.getName method, otherwise it + * returns null. + * + * @return the name of the object + */ + public String getName() { + if (dataSource != null) + return dataSource.getName(); + else + return null; + } + + /** + * Return the MIME type of this object as retrieved from + * the source object. Note that this is the full + * type with parameters. + * + * @return the MIME type + */ + public String getContentType() { + if (dataSource != null) // data source case + return dataSource.getContentType(); + else + return objectMimeType; // obj/type case + } + + /** + * Get the InputStream for this object.

+ *

+ * For DataHandlers instantiated with a DataSource, the DataHandler + * calls the DataSource.getInputStream method and + * returns the result to the caller. + *

+ * For DataHandlers instantiated with an Object, the DataHandler + * first attempts to find a DataContentHandler for the Object. If + * the DataHandler can not find a DataContentHandler for this MIME + * type, it throws an UnsupportedDataTypeException. If it is + * successful, it creates a pipe and a thread. The thread uses the + * DataContentHandler's writeTo method to write the + * stream data into one end of the pipe. The other end of the pipe + * is returned to the caller. Because a thread is created to copy + * the data, IOExceptions that may occur during the copy can not be + * propagated back to the caller. The result is an empty stream. + * + * @return the InputStream representing this data + * @throws IOException if an I/O error occurs + * @see DataContentHandler#writeTo + * @see UnsupportedDataTypeException + */ + public InputStream getInputStream() throws IOException { + InputStream ins = null; + + if (dataSource != null) { + ins = dataSource.getInputStream(); + } else { + DataContentHandler dch = getDataContentHandler(); + // we won't even try if we can't get a dch + if (dch == null) + throw new UnsupportedDataTypeException( + "no DCH for MIME type " + getBaseType()); + + if (dch instanceof ObjectDataContentHandler) { + if (((ObjectDataContentHandler) dch).getDCH() == null) + throw new UnsupportedDataTypeException( + "no object DCH for MIME type " + getBaseType()); + } + // there is none but the default^^^^^^^^^^^^^^^^ + final DataContentHandler fdch = dch; + + // from bill s. + // ce n'est pas une pipe! + // + // NOTE: This block of code needs to throw exceptions, but + // can't because it is in another thread!!! ARG! + // + final PipedOutputStream pos = new PipedOutputStream(); + PipedInputStream pin = new PipedInputStream(pos); + new Thread( + new Runnable() { + public void run() { + try { + fdch.writeTo(object, objectMimeType, pos); + } catch (IOException e) { + + } finally { + try { + pos.close(); + } catch (IOException ie) { + } + } + } + }, + "DataHandler.getInputStream").start(); + ins = pin; + } + + return ins; + } + + /** + * Write the data to an OutputStream.

+ *

+ * If the DataHandler was created with a DataSource, writeTo + * retrieves the InputStream and copies the bytes from the + * InputStream to the OutputStream passed in. + *

+ * If the DataHandler was created with an object, writeTo + * retrieves the DataContentHandler for the object's type. + * If the DataContentHandler was found, it calls the + * writeTo method on the DataContentHandler. + * + * @param os the OutputStream to write to + * @throws IOException if an I/O error occurs + */ + public void writeTo(OutputStream os) throws IOException { + // for the DataSource case + if (dataSource != null) { + InputStream is = null; + byte[] data = new byte[8 * 1024]; + int bytes_read; + + is = dataSource.getInputStream(); + + try { + while ((bytes_read = is.read(data)) > 0) { + os.write(data, 0, bytes_read); + } + } finally { + is.close(); + is = null; + } + } else { // for the Object case + DataContentHandler dch = getDataContentHandler(); + dch.writeTo(object, objectMimeType, os); + } + } + + /** + * Get an OutputStream for this DataHandler to allow overwriting + * the underlying data. + * If the DataHandler was created with a DataSource, the + * DataSource's getOutputStream method is called. + * Otherwise, null is returned. + * + * @return the OutputStream + * @throws IOException for failures creating the OutputStream + * @see DataSource#getOutputStream + * @see URLDataSource + */ + public OutputStream getOutputStream() throws IOException { + if (dataSource != null) + return dataSource.getOutputStream(); + else + return null; + } + + /** + * Return the ActivationDataFlavors in which this data is available.

+ *

+ * Returns an array of ActivationDataFlavor objects indicating the flavors + * the data can be provided in. The array is usually ordered + * according to preference for providing the data, from most + * richly descriptive to least richly descriptive.

+ *

+ * The DataHandler attempts to find a DataContentHandler that + * corresponds to the MIME type of the data. If one is located, + * the DataHandler calls the DataContentHandler's + * getTransferDataFlavors method.

+ *

+ * If a DataContentHandler can not be located, and if the + * DataHandler was created with a DataSource (or URL), one + * ActivationDataFlavor is returned that represents this object's MIME type + * and the java.io.InputStream class. If the + * DataHandler was created with an object and a MIME type, + * getTransferDataFlavors returns one ActivationDataFlavor that represents + * this object's MIME type and the object's class. + * + * @return an array of data flavors in which this data can be transferred + * @see DataContentHandler#getTransferDataFlavors + */ + public synchronized ActivationDataFlavor[] getTransferDataFlavors() { + if (factory != oldFactory) // if the factory has changed, clear cache + transferFlavors = emptyFlavors; + + // if it's not set, set it... + if (transferFlavors == emptyFlavors) + transferFlavors = getDataContentHandler().getTransferDataFlavors(); + if (transferFlavors == emptyFlavors) + return transferFlavors; // no need to clone an empty array + else + return transferFlavors.clone(); + } + + /** + * Returns whether the specified data flavor is supported + * for this object.

+ *

+ * This method iterates through the ActivationDataFlavors returned from + * getTransferDataFlavors, comparing each with + * the specified flavor. + * + * @param flavor the requested flavor for the data + * @return true if the data flavor is supported + * @see DataHandler#getTransferDataFlavors + */ + public boolean isDataFlavorSupported(ActivationDataFlavor flavor) { + ActivationDataFlavor[] lFlavors = getTransferDataFlavors(); + + for (int i = 0; i < lFlavors.length; i++) { + if (lFlavors[i].equals(flavor)) + return true; + } + return false; + } + + /** + * Returns an object that represents the data to be + * transferred. The class of the object returned is defined by the + * representation class of the data flavor.

+ * + * For DataHandler's created with DataSources or URLs:

+ *

+ * The DataHandler attempts to locate a DataContentHandler + * for this MIME type. If one is found, the passed in ActivationDataFlavor + * and the type of the data are passed to its getTransferData + * method. If the DataHandler fails to locate a DataContentHandler + * and the flavor specifies this object's MIME type and the + * java.io.InputStream class, this object's InputStream + * is returned. + * Otherwise it throws an IOException.

+ * + * For DataHandler's created with Objects:

+ *

+ * The DataHandler attempts to locate a DataContentHandler + * for this MIME type. If one is found, the passed in ActivationDataFlavor + * and the type of the data are passed to its getTransferData + * method. If the DataHandler fails to locate a DataContentHandler + * and the flavor specifies this object's MIME type and its class, + * this DataHandler's referenced object is returned. + * Otherwise it throws an IOException. + * + * @param flavor the requested flavor for the data + * @return the object + * @throws IOException if the data could not be + * converted to the requested flavor + * @throws IOException if an I/O error occurs + * @see ActivationDataFlavor + */ + public Object getTransferData(ActivationDataFlavor flavor) + throws IOException { + return getDataContentHandler().getTransferData(flavor, dataSource); + } + + /** + * Return the preferred commands for this type of data. + * This method calls the getPreferredCommands method + * in the CommandMap associated with this instance of DataHandler. + * This method returns an array that represents a subset of + * available commands. In cases where multiple commands for the + * MIME type represented by this DataHandler are present, the + * installed CommandMap chooses the appropriate commands. + * + * @return the CommandInfo objects representing the preferred commands + * @see CommandMap#getPreferredCommands + */ + public CommandInfo[] getPreferredCommands() { + if (dataSource != null) + return getCommandMap().getPreferredCommands(getBaseType(), + dataSource); + else + return getCommandMap().getPreferredCommands(getBaseType()); + } + + /** + * Return all the commands for this type of data. + * This method returns an array containing all commands + * for the type of data represented by this DataHandler. The + * MIME type for the underlying data represented by this DataHandler + * is used to call through to the getAllCommands method + * of the CommandMap associated with this DataHandler. + * + * @return the CommandInfo objects representing all the commands + * @see CommandMap#getAllCommands + */ + public CommandInfo[] getAllCommands() { + if (dataSource != null) + return getCommandMap().getAllCommands(getBaseType(), dataSource); + else + return getCommandMap().getAllCommands(getBaseType()); + } + + /** + * Get the command cmdName. Use the search semantics as + * defined by the CommandMap installed in this DataHandler. The + * MIME type for the underlying data represented by this DataHandler + * is used to call through to the getCommand method + * of the CommandMap associated with this DataHandler. + * + * @param cmdName the command name + * @return the CommandInfo corresponding to the command + * @see CommandMap#getCommand + */ + public CommandInfo getCommand(String cmdName) { + if (dataSource != null) + return getCommandMap().getCommand(getBaseType(), cmdName, + dataSource); + else + return getCommandMap().getCommand(getBaseType(), cmdName); + } + + /** + * Return the data in its preferred Object form.

+ *

+ * If the DataHandler was instantiated with an object, return + * the object.

+ *

+ * If the DataHandler was instantiated with a DataSource, + * this method uses a DataContentHandler to return the content + * object for the data represented by this DataHandler. If no + * DataContentHandler can be found for the + * the type of this data, the DataHandler returns an + * InputStream for the data. + * + * @return the content. + * @throws IOException if an IOException occurs during + * this operation. + */ + public Object getContent() throws IOException { + if (object != null) + return object; + else + return getDataContentHandler().getContent(getDataSource()); + } + + /** + * A convenience method that takes a CommandInfo object + * and instantiates the corresponding command, usually + * a JavaBean component. + *

+ * This method calls the CommandInfo's getCommandObject + * method with the ClassLoader used to load + * the jakarta.activation.DataHandler class itself. + * + * @param cmdinfo the CommandInfo corresponding to a command + * @return the instantiated command object + */ + public Object getBean(CommandInfo cmdinfo) { + Object bean = null; + + try { + // make the bean + ClassLoader cld = null; + // First try the "application's" class loader. + cld = Util.getContextClassLoader(); + if (cld == null) + cld = this.getClass().getClassLoader(); + bean = cmdinfo.getCommandObject(this, cld); + } catch (IOException | ClassNotFoundException e) { + } + + return bean; + } + + /** + * Get the DataContentHandler for this DataHandler:

+ *

+ * If a DataContentHandlerFactory is set, use it. + * Otherwise look for an object to serve DCH in the + * following order:

+ *

+ * 1) if a factory is set, use it

+ * 2) if a CommandMap is set, use it

+ * 3) use the default CommandMap

+ *

+ * In any case, wrap the real DataContentHandler with one of our own + * to handle any missing cases, fill in defaults, and to ensure that + * we always have a non-null DataContentHandler. + * + * @return the requested DataContentHandler + */ + private synchronized DataContentHandler getDataContentHandler() { + + // make sure the factory didn't change + if (factory != oldFactory) { + oldFactory = factory; + factoryDCH = null; + dataContentHandler = null; + transferFlavors = emptyFlavors; + } + + if (dataContentHandler != null) + return dataContentHandler; + + String simpleMT = getBaseType(); + + if (factoryDCH == null && factory != null) + factoryDCH = factory.createDataContentHandler(simpleMT); + + if (factoryDCH != null) + dataContentHandler = factoryDCH; + + if (dataContentHandler == null) { + if (dataSource != null) + dataContentHandler = getCommandMap(). + createDataContentHandler(simpleMT, dataSource); + else + dataContentHandler = getCommandMap(). + createDataContentHandler(simpleMT); + } + + // getDataContentHandler always uses these 'wrapper' handlers + // to make sure it returns SOMETHING meaningful... + if (dataSource != null) + dataContentHandler = new DataSourceDataContentHandler( + dataContentHandler, + dataSource); + else + dataContentHandler = new ObjectDataContentHandler( + dataContentHandler, + object, + objectMimeType); + return dataContentHandler; + } + + /** + * Use the MimeType class to extract the MIME type/subtype, + * ignoring the parameters. The type is cached. + */ + private synchronized String getBaseType() { + if (shortType == null) { + String ct = getContentType(); + try { + MimeType mt = new MimeType(ct); + shortType = mt.getBaseType(); + } catch (MimeTypeParseException e) { + shortType = ct; + } + } + return shortType; + } +} + +/** + * The DataHanderDataSource class implements the + * DataSource interface when the DataHandler is constructed + * with an Object and a mimeType string. + */ +class DataHandlerDataSource implements DataSource { + DataHandler dataHandler = null; + + /** + * The constructor. + */ + public DataHandlerDataSource(DataHandler dh) { + this.dataHandler = dh; + } + + /** + * Returns an InputStream representing this object. + * + * @return the InputStream + */ + public InputStream getInputStream() throws IOException { + return dataHandler.getInputStream(); + } + + /** + * Returns the OutputStream for this object. + * + * @return the OutputStream + */ + public OutputStream getOutputStream() throws IOException { + return dataHandler.getOutputStream(); + } + + /** + * Returns the MIME type of the data represented by this object. + * + * @return the MIME type + */ + public String getContentType() { + return dataHandler.getContentType(); + } + + /** + * Returns the name of this object. + * + * @return the name of this object + */ + public String getName() { + return dataHandler.getName(); // what else would it be? + } +} + +/* + * DataSourceDataContentHandler + * + * This is a private DataContentHandler that wraps the real + * DataContentHandler in the case where the DataHandler was instantiated + * with a DataSource. + */ +class DataSourceDataContentHandler implements DataContentHandler { + private DataSource ds = null; + private ActivationDataFlavor[] transferFlavors = null; + private DataContentHandler dch = null; + + /** + * The constructor. + */ + public DataSourceDataContentHandler(DataContentHandler dch, DataSource ds) { + this.ds = ds; + this.dch = dch; + } + + /** + * Return the ActivationDataFlavors for this + * DataContentHandler. + * + * @return the ActivationDataFlavors + */ + public ActivationDataFlavor[] getTransferDataFlavors() { + + if (transferFlavors == null) { + if (dch != null) { // is there a dch? + transferFlavors = dch.getTransferDataFlavors(); + } else { + transferFlavors = new ActivationDataFlavor[1]; + transferFlavors[0] = + new ActivationDataFlavor(ds.getContentType(), + ds.getContentType()); + } + } + return transferFlavors; + } + + /** + * Return the Transfer Data of type ActivationDataFlavor from InputStream. + * + * @param df the ActivationDataFlavor + * @param ds the DataSource + * @return the constructed Object + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws IOException { + + if (dch != null) + return dch.getTransferData(df, ds); + else if (df.equals(getTransferDataFlavors()[0])) // only have one now + return ds.getInputStream(); + else + throw new IOException("Unsupported DataFlavor: " + df); + } + + public Object getContent(DataSource ds) throws IOException { + + if (dch != null) + return dch.getContent(ds); + else + return ds.getInputStream(); + } + + /** + * Write the object to the output stream. + */ + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (dch != null) + dch.writeTo(obj, mimeType, os); + else + throw new UnsupportedDataTypeException( + "no DCH for content type " + ds.getContentType()); + } +} + +/* + * ObjectDataContentHandler + * + * This is a private DataContentHandler that wraps the real + * DataContentHandler in the case where the DataHandler was instantiated + * with an object. + */ +class ObjectDataContentHandler implements DataContentHandler { + private ActivationDataFlavor[] transferFlavors = null; + private Object obj; + private String mimeType; + private DataContentHandler dch = null; + + /** + * The constructor. + */ + public ObjectDataContentHandler(DataContentHandler dch, + Object obj, String mimeType) { + this.obj = obj; + this.mimeType = mimeType; + this.dch = dch; + } + + /** + * Return the DataContentHandler for this object. + * Used only by the DataHandler class. + */ + public DataContentHandler getDCH() { + return dch; + } + + /** + * Return the ActivationDataFlavors for this + * DataContentHandler. + * + * @return the ActivationDataFlavors + */ + public synchronized ActivationDataFlavor[] getTransferDataFlavors() { + if (transferFlavors == null) { + if (dch != null) { + transferFlavors = dch.getTransferDataFlavors(); + } else { + transferFlavors = new ActivationDataFlavor[1]; + transferFlavors[0] = new ActivationDataFlavor(obj.getClass(), + mimeType, mimeType); + } + } + return transferFlavors; + } + + /** + * Return the Transfer Data of type ActivationDataFlavor from InputStream. + * + * @param df the ActivationDataFlavor + * @param ds the DataSource + * @return the constructed Object + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws IOException { + + if (dch != null) + return dch.getTransferData(df, ds); + else if (df.equals(getTransferDataFlavors()[0])) // only have one now + return obj; + else + throw new IOException("Unsupported DataFlavor: " + df); + + } + + public Object getContent(DataSource ds) { + return obj; + } + + /** + * Write the object to the output stream. + */ + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (dch != null) + dch.writeTo(obj, mimeType, os); + else if (obj instanceof byte[]) + os.write((byte[]) obj); + else if (obj instanceof String) { + OutputStreamWriter osw = new OutputStreamWriter(os, Charset.defaultCharset()); + osw.write((String) obj); + osw.flush(); + } else + throw new UnsupportedDataTypeException( + "no object DCH for MIME type " + this.mimeType); + } +} diff --git a/net-mail/src/main/java/jakarta/activation/DataSource.java b/net-mail/src/main/java/jakarta/activation/DataSource.java new file mode 100644 index 0000000..1dd1c9a --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/DataSource.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * The DataSource interface provides Jakarta Activation + * with an abstraction of an arbitrary collection of data. It + * provides a type for that data as well as access + * to it in the form of InputStreams and + * OutputStreams where appropriate. + */ + +public interface DataSource { + + /** + * This method returns an InputStream representing + * the data and throws the appropriate exception if it can + * not do so. Note that a new InputStream object must be + * returned each time this method is called, and the stream must be + * positioned at the beginning of the data. + * + * @return an InputStream + * @throws IOException for failures creating the InputStream + */ + InputStream getInputStream() throws IOException; + + /** + * This method returns an OutputStream where the + * data can be written and throws the appropriate exception if it can + * not do so. Note that a new OutputStream object must + * be returned each time this method is called, and the stream must + * be positioned at the location the data is to be written. + * + * @return an OutputStream + * @throws IOException for failures creating the OutputStream + */ + OutputStream getOutputStream() throws IOException; + + /** + * This method returns the MIME type of the data in the form of a + * string. It should always return a valid type. It is suggested + * that getContentType return "application/octet-stream" if the + * DataSource implementation can not determine the data type. + * + * @return the MIME Type + */ + String getContentType(); + + /** + * Return the name of this object where the name of the object + * is dependant on the nature of the underlying objects. DataSources + * encapsulating files may choose to return the filename of the object. + * (Typically this would be the last component of the filename, not an + * entire pathname.) + * + * @return the name of the object. + */ + String getName(); +} diff --git a/net-mail/src/main/java/jakarta/activation/FactoryFinder.java b/net-mail/src/main/java/jakarta/activation/FactoryFinder.java new file mode 100644 index 0000000..e2f5075 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/FactoryFinder.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2021, 2024 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 jakarta.activation; + +import java.util.Arrays; +import java.util.logging.Level; +import java.util.logging.Logger; + +class FactoryFinder { + + private static final Logger logger = Logger.getLogger("jakarta.activation"); + + private static final ServiceLoaderUtil.ExceptionHandler EXCEPTION_HANDLER = + new ServiceLoaderUtil.ExceptionHandler<>() { + @Override + public RuntimeException createException(Throwable throwable, String message) { + return new IllegalStateException(message, throwable); + } + }; + + /** + * Finds the implementation {@code Class} object for the given + * factory type. + *

+ * This method is package private so that this code can be shared. + * + * @param factoryClass factory abstract class or interface to be found + * @return the {@code Class} object of the specified message factory; + * may not be {@code null} + * @throws IllegalStateException if there is no factory found + */ + static T find(Class factoryClass) throws RuntimeException { + for (ClassLoader l : getClassLoaders(Thread.class, FactoryFinder.class, System.class)) { + T f = find(factoryClass, l); + if (f != null) { + return f; + } + } + throw EXCEPTION_HANDLER.createException(null, "Provider for " + factoryClass.getName() + " cannot be found"); + } + + static T find(Class factoryClass, ClassLoader loader) throws RuntimeException { + String className = fromSystemProperty(factoryClass.getName()); + if (className != null) { + T result = newInstance(className, factoryClass, loader); + if (result != null) { + return result; + } + } + return ServiceLoaderUtil.firstByServiceLoader(factoryClass, loader, logger, EXCEPTION_HANDLER); + } + + private static T newInstance(String className, + Class service, ClassLoader loader) + throws RuntimeException { + return ServiceLoaderUtil.newInstance(className, service, loader, EXCEPTION_HANDLER); + } + + private static String fromSystemProperty(String factoryId) { + return getSystemProperty(factoryId); + } + + private static String getSystemProperty(final String property) { + logger.log(Level.FINE, "Checking system property {0}", property); + String value = System.getProperty(property); + logFound(value); + return value; + } + + private static void logFound(String value) { + if (value != null) { + logger.log(Level.FINE, " found {0}", value); + } else { + logger.log(Level.FINE, " not found"); + } + } + + private static ClassLoader[] getClassLoaders(final Class... classes) { + ClassLoader[] loaders = new ClassLoader[classes.length]; + int w = 0; + for (Class k : classes) { + ClassLoader cl = null; + if (k == Thread.class) { + cl = Thread.currentThread().getContextClassLoader(); + } else if (k == System.class) { + cl = ClassLoader.getSystemClassLoader(); + } else { + cl = k.getClassLoader(); + } + if (cl != null) { + loaders[w++] = cl; + } + } + if (loaders.length != w) { + loaders = Arrays.copyOf(loaders, w); + } + return loaders; + } +} diff --git a/net-mail/src/main/java/jakarta/activation/FileDataSource.java b/net-mail/src/main/java/jakarta/activation/FileDataSource.java new file mode 100644 index 0000000..2882188 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/FileDataSource.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) 1997, 2023, 2023 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 jakarta.activation; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * The FileDataSource class implements a simple DataSource object + * that encapsulates a file. It provides data typing services via + * a FileTypeMap object.

+ * + * FileDataSource Typing Semantics

+ *

+ * The FileDataSource class delegates data typing of files + * to an object subclassed from the FileTypeMap class. + * The setFileTypeMap method can be used to explicitly + * set the FileTypeMap for an instance of FileDataSource. If no + * FileTypeMap is set, the FileDataSource will call the FileTypeMap's + * getDefaultFileTypeMap method to get the System's default FileTypeMap. + *

+ * API Note: + * It is recommended to construct a {@code FileDataSource} using a {@code Path} + * instead of using a {@code File} since {@code Path} contains enhanced functionality. + * + * @see DataSource + * @see FileTypeMap + * @see MimetypesFileTypeMap + */ +public class FileDataSource implements DataSource { + + // keep track of original 'ref' passed in, non-null + // one indicated which was passed in: + private Path _path = null; + private FileTypeMap typeMap = null; + + /** + * Creates a FileDataSource from a File object. Note: + * The file will not actually be opened until a method is + * called that requires the file to be opened. + *

+ * API Note: + * {@code FileDataSource(Path)} constructor should be preferred over this one. + * + * @param file the file + */ + public FileDataSource(File file) { + _path = file.toPath(); // save the file Object... + } + + /** + * Creates a FileDataSource from a Path object. Note: The file will not + * actually be opened until a method is called that requires the file to be + * opened. + * + * @param path the file + */ + public FileDataSource(Path path) { + _path = path; + } + + /** + * Creates a FileDataSource from + * the specified path name. Note: + * The file will not actually be opened until a method is + * called that requires the file to be opened. + * + * @param name the system-dependent file name. + */ + public FileDataSource(String name) { + this(Paths.get(name)); // use the file constructor + } + + /** + * This method will return an InputStream representing the + * the data and will throw an IOException if it can + * not do so. This method will return a new + * instance of InputStream with each invocation. + * + * @return an InputStream + */ + public InputStream getInputStream() throws IOException { + return Files.newInputStream(_path); + } + + /** + * This method will return an OutputStream representing the + * the data and will throw an IOException if it can + * not do so. This method will return a new instance of + * OutputStream with each invocation. + * + * @return an OutputStream + */ + public OutputStream getOutputStream() throws IOException { + return Files.newOutputStream(_path); + } + + /** + * This method returns the MIME type of the data in the form of a + * string. This method uses the currently installed FileTypeMap. If + * there is no FileTypeMap explicitly set, the FileDataSource will + * call the getDefaultFileTypeMap method on + * FileTypeMap to acquire a default FileTypeMap. Note: By + * default, the FileTypeMap used will be a MimetypesFileTypeMap. + * + * @return the MIME Type + * @see FileTypeMap#getDefaultFileTypeMap + */ + public String getContentType() { + // check to see if the type map is null? + if (typeMap == null) + return FileTypeMap.getDefaultFileTypeMap().getContentType(_path); + else + return typeMap.getContentType(_path); + } + + /** + * Return the name of this object. The FileDataSource + * will return the file name of the object. + * + * @return the name of the object. + * @see DataSource + */ + public String getName() { + return _path.getFileName().toString(); + } + + /** + * Return the File object that corresponds to this FileDataSource. + * + * @return the File object for the file represented by this object. + */ + public File getFile() { + return _path.toFile(); + } + + /** + * Return the Path object that corresponds to this FileDataSource. + * + * @return the Path object for the file represented by this object. + */ + public Path getPath() { + return _path; + } + + /** + * Set the FileTypeMap to use with this FileDataSource + * + * @param map The FileTypeMap for this object. + */ + public void setFileTypeMap(FileTypeMap map) { + typeMap = map; + } +} diff --git a/net-mail/src/main/java/jakarta/activation/FileTypeMap.java b/net-mail/src/main/java/jakarta/activation/FileTypeMap.java new file mode 100644 index 0000000..1359f77 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/FileTypeMap.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 1997, 2023, 2023 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 jakarta.activation; + +import java.io.File; +import java.nio.file.Path; +import java.util.Map; +import java.util.WeakHashMap; + +/** + * The FileTypeMap is an abstract class that provides a data typing + * interface for files. Implementations of this class will + * implement the getContentType methods which will derive a content + * type from a file name or a File object. FileTypeMaps could use any + * scheme to determine the data type, from examining the file extension + * of a file (like the MimetypesFileTypeMap) to opening the file and + * trying to derive its type from the contents of the file. The + * FileDataSource class uses the default FileTypeMap (a MimetypesFileTypeMap + * unless changed) to determine the content type of files. + * + * @see FileTypeMap + * @see FileDataSource + * @see MimetypesFileTypeMap + */ + +public abstract class FileTypeMap { + + private static FileTypeMap defaultMap = null; + + private static Map map = new WeakHashMap<>(); + + /** + * The default constructor. + */ + public FileTypeMap() { + super(); + } + + /** + * Return the default FileTypeMap for the system. + * If setDefaultFileTypeMap was called, return + * that instance, otherwise return an instance of + * MimetypesFileTypeMap. + * + * @return The default FileTypeMap + * @see FileTypeMap#setDefaultFileTypeMap + */ + public static synchronized FileTypeMap getDefaultFileTypeMap() { + if (defaultMap != null) { + return defaultMap; + } + ClassLoader tccl = Util.getContextClassLoader(); + FileTypeMap def = map.get(tccl); + if (def == null) { + def = new MimetypesFileTypeMap(); + map.put(tccl, def); + } + return def; + } + + /** + * Sets the default FileTypeMap for the system. This instance + * will be returned to callers of getDefaultFileTypeMap. + * + * @param fileTypeMap The FileTypeMap. + */ + public static synchronized void setDefaultFileTypeMap(FileTypeMap fileTypeMap) { + map.remove(Util.getContextClassLoader()); + defaultMap = fileTypeMap; + } + + /** + * Return the type of the file object. This method should + * always return a valid MIME type. + * + * @param file A file to be typed. + * @return The content type. + */ + abstract public String getContentType(File file); + + /** + * Return the type of the file Path object. This method should + * always return a valid MIME type. + * + * @param path A file Path to be typed. + * @return The content type. + */ + abstract public String getContentType(Path path); + + /** + * Return the type of the file passed in. This method should + * always return a valid MIME type. + * + * @param filename the pathname of the file. + * @return The content type. + */ + abstract public String getContentType(String filename); +} diff --git a/net-mail/src/main/java/jakarta/activation/MailcapCommandMap.java b/net-mail/src/main/java/jakarta/activation/MailcapCommandMap.java new file mode 100644 index 0000000..75715e2 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/MailcapCommandMap.java @@ -0,0 +1,667 @@ +/* + * Copyright (c) 1997, 2024 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 jakarta.activation; + +import jakarta.activation.spi.MailcapRegistryProvider; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.ServiceConfigurationError; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * MailcapCommandMap extends the CommandMap + * abstract class. It implements a CommandMap whose configuration + * is based on mailcap files + * (RFC 1524). + * The MailcapCommandMap can be configured both programmatically + * and via configuration files. + *

+ * Mailcap file search order:

+ * The MailcapCommandMap looks in various places in the user's + * system for mailcap file entries. When requests are made + * to search for commands in the MailcapCommandMap, it searches + * mailcap files in the following order: + *

    + *
  1. Programatically added entries to the MailcapCommandMap instance. + *
  2. The file .mailcap in the user's home directory. + *
  3. The file mailcap in the Java runtime. + *
  4. The file or resources named META-INF/mailcap. + *
  5. The file or resource named META-INF/mailcap.default + * (usually found only in the activation.jar file). + *
+ *

+ * (The current implementation looks for the mailcap file + * in the Java runtime in the directory java.home/conf + * if it exists, and otherwise in the directory + * java.home/lib, where java.home is the value + * of the "java.home" System property. Note that the "conf" directory was + * introduced in JDK 9.) + *

+ * Mailcap file format:

+ *

+ * Mailcap files must conform to the mailcap + * file specification (RFC 1524, A User Agent Configuration Mechanism + * For Multimedia Mail Format Information). + * The file format consists of entries corresponding to + * particular MIME types. In general, the specification + * specifies applications for clients to use when they + * themselves cannot operate on the specified MIME type. The + * MailcapCommandMap extends this specification by using a parameter mechanism + * in mailcap files that allows JavaBeans(tm) components to be specified as + * corresponding to particular commands for a MIME type.

+ *

+ * When a mailcap file is + * parsed, the MailcapCommandMap recognizes certain parameter signatures, + * specifically those parameter names that begin with x-java-. + * The MailcapCommandMap uses this signature to find + * command entries for inclusion into its registries. + * Parameter names with the form x-java-<name> + * are read by the MailcapCommandMap as identifying a command + * with the name name. When the name is + * content-handler the MailcapCommandMap recognizes the class + * signified by this parameter as a DataContentHandler. + * All other commands are handled generically regardless of command + * name. The command implementation is specified by a fully qualified + * class name of a JavaBean(tm) component. For example; a command for viewing + * some data can be specified as: x-java-view=com.foo.ViewBean.

+ *

+ * When the command name is fallback-entry, the value of + * the command may be true or false. An + * entry for a MIME type that includes a parameter of + * x-java-fallback-entry=true defines fallback commands + * for that MIME type that will only be used if no non-fallback entry + * can be found. For example, an entry of the form text/*; ; + * x-java-fallback-entry=true; x-java-view=com.sun.TextViewer + * specifies a view command to be used for any text MIME type. This + * view command would only be used if a non-fallback view command for + * the MIME type could not be found.

+ *

+ * MailcapCommandMap aware mailcap files have the + * following general form:

+ * + * # Comments begin with a '#' and continue to the end of the line.
+ * <mime type>; ; <parameter list>
+ * # Where a parameter list consists of one or more parameters,
+ * # where parameters look like: x-java-view=com.sun.TextViewer
+ * # and a parameter list looks like:
+ * text/plain; ; x-java-view=com.sun.TextViewer; x-java-edit=com.sun.TextEdit + *
+ * # Note that mailcap entries that do not contain 'x-java' parameters
+ * # and comply to RFC 1524 are simply ignored:
+ * image/gif; /usr/dt/bin/sdtimage %s
+ * + *
+ * + * @author Bart Calder + * @author Bill Shannon + */ + +public class MailcapCommandMap extends CommandMap { + + private static final Logger logger = Logger.getLogger(MailcapCommandMap.class.getName()); + + private static final int PROG = 0; // programmatically added entries + private static final String confDir; + + static { + String dir = null; + String home = System.getProperty("java.home"); + String newdir = home + File.separator + "conf"; + File conf = new File(newdir); + if (conf.exists()) + dir = newdir + File.separator; + else + dir = home + File.separator + "lib" + File.separator; + confDir = dir; + } + + /* + * We manage a collection of databases, searched in order. + */ + private MailcapRegistry[] DB; + + /** + * The default Constructor. + */ + public MailcapCommandMap() { + super(); + List dbv = new ArrayList<>(5); // usually 5 or less databases + MailcapRegistry mf = null; + dbv.add(null); // place holder for PROG entry + + logger.log(Level.FINE, "MailcapCommandMap: load HOME"); + String user_home = System.getProperty("user.home"); + + if (user_home != null) { + String path = user_home + File.separator + ".mailcap"; + mf = loadFile(path); + if (mf != null) + dbv.add(mf); + } + + logger.log(Level.FINE, "MailcapCommandMap: load SYS"); + // check system's home + if (confDir != null) { + mf = loadFile(confDir + "mailcap"); + if (mf != null) + dbv.add(mf); + } + + logger.log(Level.FINE, "MailcapCommandMap: load JAR"); + // load from the app's jar file + loadAllResources(dbv, "META-INF/mailcap"); + + logger.log(Level.FINE, "MailcapCommandMap: load DEF"); + mf = loadResource("/META-INF/mailcap.default"); + + if (mf != null) + dbv.add(mf); + + DB = new MailcapRegistry[dbv.size()]; + DB = dbv.toArray(DB); + } + + /** + * Constructor that allows the caller to specify the path + * of a mailcap file. + * + * @param fileName The name of the mailcap file to open + * @throws IOException if the file can't be accessed + */ + public MailcapCommandMap(String fileName) throws IOException { + this(); + if (DB[PROG] == null) { + try { + DB[PROG] = getImplementation().getByFileName(fileName); + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + String message = "Cannot find or load an implementation for MailcapRegistryProvider. " + + "MailcapRegistry: can't load " + fileName; + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, message, e); + } + throw new IOException(message, e); + } + } + if (DB[PROG] != null && logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "MailcapCommandMap: load PROG from " + fileName); + } + } + + /** + * Constructor that allows the caller to specify an InputStream + * containing a mailcap file. + * + * @param is InputStream of the mailcap file to open + */ + public MailcapCommandMap(InputStream is) { + this(); + + if (DB[PROG] == null) { + try { + DB[PROG] = getImplementation().getByInputStream(is); + } catch (IOException ex) { + // XXX - should throw it + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Cannot find or load an implementation for MailcapRegistryProvider." + + "MailcapRegistry: can't load InputStream", e); + } + } + } + if (DB[PROG] != null && logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "MailcapCommandMap: load PROG"); + } + } + + /** + * Load from the named resource. + */ + private MailcapRegistry loadResource(String name) { + try (InputStream clis = Util.getResourceAsStream(this.getClass(), name)) { + if (clis != null) { + MailcapRegistry mf = getImplementation().getByInputStream(clis); + logger.log(Level.FINE, "MailcapCommandMap: successfully loaded " + + "mailcap file: " + name); + return mf; + } else { + logger.log(Level.FINE, "MailcapCommandMap: not loading " + + "mailcap file: " + name); + } + } catch (IOException e) { + logger.log(Level.FINE, "MailcapCommandMap: can't load " + name, e); + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + logger.log(Level.FINE, "Cannot find or load an implementation for MailcapRegistryProvider. " + + "MailcapRegistry: can't load " + name, e); + } + return null; + } + + /** + * Load all of the named resource. + */ + private void loadAllResources(List v, String name) { + boolean anyLoaded = false; + try { + URL[] urls; + ClassLoader cld = null; + // First try the "application's" class loader. + cld = Util.getContextClassLoader(); + if (cld == null) + cld = this.getClass().getClassLoader(); + if (cld != null) + urls = Util.getResources(cld, name); + else + urls = Util.getSystemResources(name); + if (urls != null) { + logger.log(Level.FINE, "MailcapCommandMap: getResources"); + for (int i = 0; i < urls.length; i++) { + URL url = urls[i]; + logger.log(Level.FINE, "MailcapCommandMap: URL " + url); + try (InputStream clis = Util.openStream(url)) { + if (clis != null) { + v.add(getImplementation().getByInputStream(clis)); + anyLoaded = true; + logger.log(Level.FINE, "MailcapCommandMap: " + + "successfully loaded " + + "mailcap file from URL: " + + url); + } else { + logger.log(Level.FINE, "MailcapCommandMap: " + + "not loading mailcap " + + "file from URL: " + url); + } + } catch (IOException ioex) { + logger.log(Level.FINE, "MailcapCommandMap: can't load " + + url, ioex); + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + logger.log(Level.FINE, "Cannot find or load an implementation for MailcapRegistryProvider. " + + "MailcapRegistry: can't load " + name, e); + } + } + } + } catch (Exception ex) { + logger.log(Level.FINE, "MailcapCommandMap: can't load " + name, ex); + } + + // if failed to load anything, fall back to old technique, just in case + if (!anyLoaded) { + logger.log(Level.FINE, "MailcapCommandMap: !anyLoaded"); + MailcapRegistry mf = loadResource("/" + name); + if (mf != null) + v.add(mf); + } + } + + /** + * Load from the named file. + */ + private MailcapRegistry loadFile(String name) { + MailcapRegistry mtf = null; + + try { + mtf = getImplementation().getByFileName(name); + } catch (IOException e) { + logger.log(Level.FINE, "MailcapRegistry: can't load from file - " + name, e); + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + logger.log(Level.FINE, "Cannot find or load an implementation for MailcapRegistryProvider. " + + "MailcapRegistry: can't load " + name, e); + } + return mtf; + } + + /** + * Get the preferred command list for a MIME Type. The MailcapCommandMap + * searches the mailcap files as described above under + * Mailcap file search order.

+ *

+ * The result of the search is a proper subset of available + * commands in all mailcap files known to this instance of + * MailcapCommandMap. The first entry for a particular command + * is considered the preferred command. + * + * @param mimeType the MIME type + * @return the CommandInfo objects representing the preferred commands. + */ + public synchronized CommandInfo[] getPreferredCommands(String mimeType) { + List cmdList = new ArrayList<>(); + if (mimeType != null) + mimeType = mimeType.toLowerCase(Locale.ENGLISH); + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map> cmdMap = DB[i].getMailcapList(mimeType); + if (cmdMap != null) + appendPrefCmdsToList(cmdMap, cmdList); + } + + // now add the fallback commands + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map> cmdMap = DB[i].getMailcapFallbackList(mimeType); + if (cmdMap != null) + appendPrefCmdsToList(cmdMap, cmdList); + } + + CommandInfo[] cmdInfos = new CommandInfo[cmdList.size()]; + cmdInfos = cmdList.toArray(cmdInfos); + + return cmdInfos; + } + + /** + * Put the commands that are in the hash table, into the list. + */ + private void appendPrefCmdsToList(Map> cmdHash, List cmdList) { + Iterator verb_enum = cmdHash.keySet().iterator(); + + while (verb_enum.hasNext()) { + String verb = verb_enum.next(); + if (!checkForVerb(cmdList, verb)) { + List cmdList2 = cmdHash.get(verb); // get the list + String className = cmdList2.get(0); + cmdList.add(new CommandInfo(verb, className)); + } + } + } + + /** + * Check the cmdList to see if this command exists, return + * true if the verb is there. + */ + private boolean checkForVerb(List cmdList, String verb) { + Iterator ee = cmdList.iterator(); + while (ee.hasNext()) { + String enum_verb = (ee.next()).getCommandName(); + if (enum_verb.equals(verb)) + return true; + } + return false; + } + + /** + * Get all the available commands in all mailcap files known to + * this instance of MailcapCommandMap for this MIME type. + * + * @param mimeType the MIME type + * @return the CommandInfo objects representing all the commands. + */ + public synchronized CommandInfo[] getAllCommands(String mimeType) { + List cmdList = new ArrayList<>(); + if (mimeType != null) + mimeType = mimeType.toLowerCase(Locale.ENGLISH); + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map> cmdMap = DB[i].getMailcapList(mimeType); + if (cmdMap != null) + appendCmdsToList(cmdMap, cmdList); + } + + // now add the fallback commands + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map> cmdMap = DB[i].getMailcapFallbackList(mimeType); + if (cmdMap != null) + appendCmdsToList(cmdMap, cmdList); + } + + CommandInfo[] cmdInfos = new CommandInfo[cmdList.size()]; + cmdInfos = cmdList.toArray(cmdInfos); + + return cmdInfos; + } + + /** + * Put the commands that are in the hash table, into the list. + */ + private void appendCmdsToList(Map> typeHash, List cmdList) { + Iterator verb_enum = typeHash.keySet().iterator(); + + while (verb_enum.hasNext()) { + String verb = verb_enum.next(); + List cmdList2 = typeHash.get(verb); + Iterator cmd_enum = cmdList2.iterator(); + + while (cmd_enum.hasNext()) { + String cmd = cmd_enum.next(); + cmdList.add(new CommandInfo(verb, cmd)); + // cmdList.add(0, new CommandInfo(verb, cmd)); + } + } + } + + /** + * Get the command corresponding to cmdName for the MIME type. + * + * @param mimeType the MIME type + * @param cmdName the command name + * @return the CommandInfo object corresponding to the command. + */ + public synchronized CommandInfo getCommand(String mimeType, + String cmdName) { + if (mimeType != null) + mimeType = mimeType.toLowerCase(Locale.ENGLISH); + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map> cmdMap = DB[i].getMailcapList(mimeType); + if (cmdMap != null) { + // get the cmd list for the cmd + List v = cmdMap.get(cmdName); + if (v != null) { + String cmdClassName = v.get(0); + + if (cmdClassName != null) + return new CommandInfo(cmdName, cmdClassName); + } + } + } + + // now try the fallback list + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + Map> cmdMap = DB[i].getMailcapFallbackList(mimeType); + if (cmdMap != null) { + // get the cmd list for the cmd + List v = cmdMap.get(cmdName); + if (v != null) { + String cmdClassName = v.get(0); + + if (cmdClassName != null) + return new CommandInfo(cmdName, cmdClassName); + } + } + } + return null; + } + + /** + * Add entries to the registry. Programmatically + * added entries are searched before other entries.

+ *

+ * The string that is passed in should be in mailcap + * format. + * + * @param mail_cap a correctly formatted mailcap string + */ + public synchronized void addMailcap(String mail_cap) { + // check to see if one exists + logger.log(Level.FINE, "MailcapCommandMap: add to PROG"); + try { + if (DB[PROG] == null) { + DB[PROG] = getImplementation().getInMemory(); + } + DB[PROG].appendToMailcap(mail_cap); + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + logger.log(Level.FINE, "Cannot find or load an implementation for MailcapRegistryProvider. " + + "MailcapRegistry: can't load", e); + throw e; + } + } + + /** + * Return the DataContentHandler for the specified MIME type. + * + * @param mimeType the MIME type + * @return the DataContentHandler + */ + public synchronized DataContentHandler createDataContentHandler( + String mimeType) { + logger.log(Level.FINE, "MailcapCommandMap: createDataContentHandler for " + mimeType); + if (mimeType != null) + mimeType = mimeType.toLowerCase(Locale.ENGLISH); + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + logger.log(Level.FINE, " search DB #" + i); + Map> cmdMap = DB[i].getMailcapList(mimeType); + if (cmdMap != null) { + List v = cmdMap.get("content-handler"); + if (v != null) { + String name = v.get(0); + DataContentHandler dch = getDataContentHandler(name); + if (dch != null) + return dch; + } + } + } + + // now try the fallback entries + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + logger.log(Level.FINE, " search fallback DB #" + i); + Map> cmdMap = DB[i].getMailcapFallbackList(mimeType); + if (cmdMap != null) { + List v = cmdMap.get("content-handler"); + if (v != null) { + String name = v.get(0); + DataContentHandler dch = getDataContentHandler(name); + if (dch != null) + return dch; + } + } + } + return null; + } + + private DataContentHandler getDataContentHandler(String name) { + logger.log(Level.FINE, " got content-handler"); + logger.log(Level.FINE, " class " + name); + try { + ClassLoader cld = null; + // First try the "application's" class loader. + cld = Util.getContextClassLoader(); + if (cld == null) + cld = this.getClass().getClassLoader(); + Class cl = null; + try { + cl = cld.loadClass(name); + } catch (Exception ex) { + // if anything goes wrong, do it the old way + cl = Class.forName(name); + } + if (cl != null) // XXX - always true? + return (DataContentHandler) + cl.getConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + logger.log(Level.FINE, "Can't load DCH " + name, e); + } + return null; + } + + /** + * Get all the MIME types known to this command map. + * + * @return array of MIME types as strings + * @since JAF 1.1 + */ + public synchronized String[] getMimeTypes() { + List mtList = new ArrayList<>(); + + for (int i = 0; i < DB.length; i++) { + if (DB[i] == null) + continue; + String[] ts = DB[i].getMimeTypes(); + if (ts != null) { + for (int j = 0; j < ts.length; j++) { + // eliminate duplicates + if (!mtList.contains(ts[j])) + mtList.add(ts[j]); + } + } + } + + String[] mts = new String[mtList.size()]; + mts = mtList.toArray(mts); + + return mts; + } + + /** + * Get the native commands for the given MIME type. + * Returns an array of strings where each string is + * an entire mailcap file entry. The application + * will need to parse the entry to extract the actual + * command as well as any attributes it needs. See + * RFC 1524 + * for details of the mailcap entry syntax. Only mailcap + * entries that specify a view command for the specified + * MIME type are returned. + * + * @param mimeType the MIME type + * @return array of native command entries + * @since JAF 1.1 + */ + public synchronized String[] getNativeCommands(String mimeType) { + List cmdList = new ArrayList<>(); + if (mimeType != null) { + mimeType = mimeType.toLowerCase(Locale.ENGLISH); + } + for (MailcapRegistry mailcapRegistry : DB) { + if (mailcapRegistry == null) { + continue; + } + String[] cmds = mailcapRegistry.getNativeCommands(mimeType); + if (cmds != null) { + for (String cmd : cmds) { + if (!cmdList.contains(cmd)) { + cmdList.add(cmd); + } + } + } + } + String[] cmds = new String[cmdList.size()]; + cmds = cmdList.toArray(cmds); + + return cmds; + } + + private MailcapRegistryProvider getImplementation() { + return FactoryFinder.find(MailcapRegistryProvider.class); + } +} diff --git a/net-mail/src/main/java/jakarta/activation/MailcapRegistry.java b/net-mail/src/main/java/jakarta/activation/MailcapRegistry.java new file mode 100644 index 0000000..e22f7fb --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/MailcapRegistry.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2021 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 jakarta.activation; + +import java.util.List; +import java.util.Map; + +/** + * The MailcapRegistry interface is implemented by objects that can + * be used to store and retrieve MailcapEntries. + *

+ * Application must implement {@link jakarta.activation.spi.MailcapRegistryProvider} + * to create new instances of the MailcapRegistry. Implementation of the MailcapRegistry + * can store MailcapEntries in different ways and that storage must be accessible through the + * {@link jakarta.activation.spi.MailcapRegistryProvider} methods. + * Implementation of the MailcapRegistry must contain in-memory storage for MailcapEntries. + */ +public interface MailcapRegistry { + + /** + * 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. + * + * @param mime_type the MIME type + * @return the map of MailcapEntries + */ + Map> getMailcapList(String mime_type); + + /** + * 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. + * + * @param mime_type the MIME type + * @return the map of fallback MailcapEntries + */ + Map> getMailcapFallbackList(String mime_type); + + /** + * Return all the MIME types known to this mailcap file. + * + * @return a String array of the MIME types + */ + String[] getMimeTypes(); + + /** + * Return all the native comands for the given MIME type. + * + * @param mime_type the MIME type + * @return a String array of the commands + */ + String[] getNativeCommands(String mime_type); + + /** + * 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 + * + * @param mail_cap the mailcap string + */ + void appendToMailcap(String mail_cap); +} diff --git a/net-mail/src/main/java/jakarta/activation/MimeType.java b/net-mail/src/main/java/jakarta/activation/MimeType.java new file mode 100644 index 0000000..fa65104 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/MimeType.java @@ -0,0 +1,275 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +import java.util.Locale; + +/** + * A Multipurpose Internet Mail Extension (MIME) type, as defined + * in RFC 2045 and 2046. + */ +public class MimeType { + + /** + * A string that holds all the special chars. + */ + private static final String TSPECIALS = "()<>@,;:/[]?=\\\""; + private String primaryType; + private String subType; + private MimeTypeParameterList parameters; + + /** + * Default constructor. + */ + public MimeType() { + primaryType = "application"; + subType = "*"; + parameters = new MimeTypeParameterList(); + } + + /** + * Constructor that builds a MimeType from a String. + * + * @param rawdata the MIME type string + * @throws MimeTypeParseException if the MIME type can't be parsed + */ + public MimeType(String rawdata) throws MimeTypeParseException { + parse(rawdata); + } + + /** + * Constructor that builds a MimeType with the given primary and sub type + * but has an empty parameter list. + * + * @param primary the primary MIME type + * @param sub the MIME sub-type + * @throws MimeTypeParseException if the primary type or subtype + * is not a valid token + */ + public MimeType(String primary, String sub) throws MimeTypeParseException { + // check to see if primary is valid + if (isValidToken(primary)) { + primaryType = primary.toLowerCase(Locale.ENGLISH); + } else { + throw new MimeTypeParseException("Primary type is invalid."); + } + + // check to see if sub is valid + if (isValidToken(sub)) { + subType = sub.toLowerCase(Locale.ENGLISH); + } else { + throw new MimeTypeParseException("Sub type is invalid."); + } + + parameters = new MimeTypeParameterList(); + } + + /** + * Determine whether or not a given character belongs to a legal token. + */ + private static boolean isTokenChar(char c) { + return ((c > 040) && (c < 0177)) && (TSPECIALS.indexOf(c) < 0); + } + + /** + * A routine for parsing the MIME type out of a String. + */ + private void parse(String rawdata) throws MimeTypeParseException { + int slashIndex = rawdata.indexOf('/'); + int semIndex = rawdata.indexOf(';'); + if ((slashIndex < 0) && (semIndex < 0)) { + // neither character is present, so treat it + // as an error + throw new MimeTypeParseException("Unable to find a sub type."); + } else if ((slashIndex < 0) && (semIndex >= 0)) { + // we have a ';' (and therefore a parameter list), + // but no '/' indicating a sub type is present + throw new MimeTypeParseException("Unable to find a sub type."); + } else if ((slashIndex >= 0) && (semIndex < 0)) { + // we have a primary and sub type but no parameter list + primaryType = rawdata.substring(0, slashIndex).trim(). + toLowerCase(Locale.ENGLISH); + subType = rawdata.substring(slashIndex + 1).trim(). + toLowerCase(Locale.ENGLISH); + parameters = new MimeTypeParameterList(); + } else if (slashIndex < semIndex) { + // we have all three items in the proper sequence + primaryType = rawdata.substring(0, slashIndex).trim(). + toLowerCase(Locale.ENGLISH); + subType = rawdata.substring(slashIndex + 1, semIndex).trim(). + toLowerCase(Locale.ENGLISH); + parameters = new MimeTypeParameterList(rawdata.substring(semIndex)); + } else { + // we have a ';' lexically before a '/' which means we + // have a primary type and a parameter list but no sub type + throw new MimeTypeParseException("Unable to find a sub type."); + } + + // now validate the primary and sub types + + // check to see if primary is valid + if (!isValidToken(primaryType)) + throw new MimeTypeParseException("Primary type is invalid."); + + // check to see if sub is valid + if (!isValidToken(subType)) + throw new MimeTypeParseException("Sub type is invalid."); + } + + /** + * Retrieve the primary type of this object. + * + * @return the primary MIME type + */ + public String getPrimaryType() { + return primaryType; + } + + /** + * Set the primary type for this object to the given String. + * + * @param primary the primary MIME type + * @throws MimeTypeParseException if the primary type + * is not a valid token + */ + public void setPrimaryType(String primary) throws MimeTypeParseException { + // check to see if primary is valid + if (!isValidToken(primaryType)) + throw new MimeTypeParseException("Primary type is invalid."); + primaryType = primary.toLowerCase(Locale.ENGLISH); + } + + /** + * Retrieve the subtype of this object. + * + * @return the MIME subtype + */ + public String getSubType() { + return subType; + } + + /** + * Set the subtype for this object to the given String. + * + * @param sub the MIME subtype + * @throws MimeTypeParseException if the subtype + * is not a valid token + */ + public void setSubType(String sub) throws MimeTypeParseException { + // check to see if sub is valid + if (!isValidToken(subType)) + throw new MimeTypeParseException("Sub type is invalid."); + subType = sub.toLowerCase(Locale.ENGLISH); + } + + /** + * Retrieve this object's parameter list. + * + * @return a MimeTypeParameterList object representing the parameters + */ + public MimeTypeParameterList getParameters() { + return parameters; + } + + /** + * Retrieve the value associated with the given name, or null if there + * is no current association. + * + * @param name the parameter name + * @return the paramter's value + */ + public String getParameter(String name) { + return parameters.get(name); + } + + /** + * Set the value to be associated with the given name, replacing + * any previous association. + * + * @param name the parameter name + * @param value the paramter's value + */ + public void setParameter(String name, String value) { + parameters.set(name, value); + } + + /** + * Remove any value associated with the given name. + * + * @param name the parameter name + */ + public void removeParameter(String name) { + parameters.remove(name); + } + + /** + * Return the String representation of this object. + */ + public String toString() { + return getBaseType() + parameters.toString(); + } + + /** + * Return a String representation of this object + * without the parameter list. + * + * @return the MIME type and sub-type + */ + public String getBaseType() { + return primaryType + "/" + subType; + } + + /** + * Determine if the primary and sub type of this object is + * the same as what is in the given type. + * + * @param type the MimeType object to compare with + * @return true if they match + */ + public boolean match(MimeType type) { + return primaryType.equals(type.getPrimaryType()) + && (subType.equals("*") + || type.getSubType().equals("*") + || (subType.equals(type.getSubType()))); + } + + // below here be scary parsing related things + + /** + * Determine if the primary and sub type of this object is + * the same as the content type described in rawdata. + * + * @param rawdata the MIME type string to compare with + * @return true if they match + * @throws MimeTypeParseException if the MIME type can't be parsed + */ + public boolean match(String rawdata) throws MimeTypeParseException { + return match(new MimeType(rawdata)); + } + + /** + * Determine whether or not a given string is a legal token. + */ + private boolean isValidToken(String s) { + int len = s.length(); + if (len > 0) { + for (int i = 0; i < len; ++i) { + char c = s.charAt(i); + if (!isTokenChar(c)) { + return false; + } + } + return true; + } else { + return false; + } + } +} diff --git a/net-mail/src/main/java/jakarta/activation/MimeTypeEntry.java b/net-mail/src/main/java/jakarta/activation/MimeTypeEntry.java new file mode 100644 index 0000000..6f4be61 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/MimeTypeEntry.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +/** + * Represents mapping between the file extension and the MIME type string. + */ +public class MimeTypeEntry { + private String type; + private String extension; + + /** + * Create new {@code MimeTypeEntry} + * + * @param mime_type the MIME type string + * @param file_ext the file extension + */ + public MimeTypeEntry(String mime_type, String file_ext) { + type = mime_type; + extension = file_ext; + } + + /** + * Get MIME type string + * + * @return the MIME type string + */ + public String getMIMEType() { + return type; + } + + /** + * Get the file extension + * + * @return the file extension + */ + public String getFileExtension() { + return extension; + } + + @Override + public String toString() { + return "MIMETypeEntry: " + type + ", " + extension; + } +} diff --git a/net-mail/src/main/java/jakarta/activation/MimeTypeParameterList.java b/net-mail/src/main/java/jakarta/activation/MimeTypeParameterList.java new file mode 100644 index 0000000..f37eb1e --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/MimeTypeParameterList.java @@ -0,0 +1,323 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Locale; + +/** + * A parameter list of a MimeType + * as defined in RFC 2045 and 2046. The Primary type of the + * object must already be stripped off. + * + * @see MimeType + */ +public class MimeTypeParameterList { + /** + * A string that holds all the special chars. + */ + private static final String TSPECIALS = "()<>@,;:/[]?=\\\""; + private Hashtable parameters; + + + /** + * Default constructor. + */ + public MimeTypeParameterList() { + parameters = new Hashtable<>(); + } + + /** + * Constructs a new MimeTypeParameterList with the passed in data. + * + * @param parameterList an RFC 2045, 2046 compliant parameter list. + * @throws MimeTypeParseException if the MIME type can't be parsed + */ + public MimeTypeParameterList(String parameterList) + throws MimeTypeParseException { + parameters = new Hashtable<>(); + + // now parse rawdata + parse(parameterList); + } + + /** + * Determine whether or not a given character belongs to a legal token. + */ + private static boolean isTokenChar(char c) { + return ((c > 040) && (c < 0177)) && (TSPECIALS.indexOf(c) < 0); + } + + /** + * return the index of the first non white space character in + * rawdata at or after index i. + */ + private static int skipWhiteSpace(String rawdata, int i) { + int length = rawdata.length(); + while ((i < length) && Character.isWhitespace(rawdata.charAt(i))) + i++; + return i; + } + + /** + * A routine that knows how and when to quote and escape the given value. + */ + private static String quote(String value) { + boolean needsQuotes = false; + + // check to see if we actually have to quote this thing + int length = value.length(); + for (int i = 0; (i < length) && !needsQuotes; i++) { + needsQuotes = !isTokenChar(value.charAt(i)); + } + + if (needsQuotes) { + StringBuilder buffer = new StringBuilder(); + buffer.ensureCapacity((int) (length * 1.5)); + + // add the initial quote + buffer.append('"'); + + // add the properly escaped text + for (int i = 0; i < length; ++i) { + char c = value.charAt(i); + if ((c == '\\') || (c == '"')) + buffer.append('\\'); + buffer.append(c); + } + + // add the closing quote + buffer.append('"'); + + return buffer.toString(); + } else { + return value; + } + } + + /** + * A routine that knows how to strip the quotes and + * escape sequences from the given value. + */ + private static String unquote(String value) { + int valueLength = value.length(); + StringBuilder buffer = new StringBuilder(); + buffer.ensureCapacity(valueLength); + + boolean escaped = false; + for (int i = 0; i < valueLength; ++i) { + char currentChar = value.charAt(i); + if (!escaped && (currentChar != '\\')) { + buffer.append(currentChar); + } else if (escaped) { + buffer.append(currentChar); + escaped = false; + } else { + escaped = true; + } + } + + return buffer.toString(); + } + + /** + * A routine for parsing the parameter list out of a String. + * + * @param parameterList an RFC 2045, 2046 compliant parameter list. + * @throws MimeTypeParseException if the MIME type can't be parsed + */ + private void parse(String parameterList) throws MimeTypeParseException { + if (parameterList == null) + return; + + int length = parameterList.length(); + if (length == 0) + return; + + int i; + char c; + for (i = skipWhiteSpace(parameterList, 0); + i < length && (c = parameterList.charAt(i)) == ';'; + i = skipWhiteSpace(parameterList, i)) { + int lastIndex; + String name; + String value; + + // eat the ';' + i++; + + // now parse the parameter name + + // skip whitespace + i = skipWhiteSpace(parameterList, i); + + // tolerate trailing semicolon, even though it violates the spec + if (i >= length) + return; + + // find the end of the token char run + lastIndex = i; + while ((i < length) && isTokenChar(parameterList.charAt(i))) + i++; + + name = parameterList.substring(lastIndex, i). + toLowerCase(Locale.ENGLISH); + + // now parse the '=' that separates the name from the value + i = skipWhiteSpace(parameterList, i); + + if (i >= length || parameterList.charAt(i) != '=') + throw new MimeTypeParseException( + "Couldn't find the '=' that separates a " + + "parameter name from its value."); + + // eat it and parse the parameter value + i++; + i = skipWhiteSpace(parameterList, i); + + if (i >= length) + throw new MimeTypeParseException( + "Couldn't find a value for parameter named " + name); + + // now find out whether or not we have a quoted value + c = parameterList.charAt(i); + if (c == '"') { + // yup it's quoted so eat it and capture the quoted string + i++; + if (i >= length) + throw new MimeTypeParseException( + "Encountered unterminated quoted parameter value."); + + lastIndex = i; + + // find the next unescaped quote + while (i < length) { + c = parameterList.charAt(i); + if (c == '"') + break; + if (c == '\\') { + // found an escape sequence + // so skip this and the + // next character + i++; + } + i++; + } + if (c != '"') + throw new MimeTypeParseException( + "Encountered unterminated quoted parameter value."); + + value = unquote(parameterList.substring(lastIndex, i)); + // eat the quote + i++; + } else if (isTokenChar(c)) { + // nope it's an ordinary token so it + // ends with a non-token char + lastIndex = i; + while (i < length && isTokenChar(parameterList.charAt(i))) + i++; + value = parameterList.substring(lastIndex, i); + } else { + // it ain't a value + throw new MimeTypeParseException( + "Unexpected character encountered at index " + i); + } + + // now put the data into the hashtable + parameters.put(name, value); + } + if (i < length) { + throw new MimeTypeParseException( + "More characters encountered in input than expected."); + } + } + + /** + * Return the number of name-value pairs in this list. + * + * @return the number of parameters + */ + public int size() { + return parameters.size(); + } + + /** + * Determine whether or not this list is empty. + * + * @return true if there are no parameters + */ + public boolean isEmpty() { + return parameters.isEmpty(); + } + + /** + * Retrieve the value associated with the given name, or null if there + * is no current association. + * + * @param name the parameter name + * @return the parameter's value + */ + public String get(String name) { + return parameters.get(name.trim().toLowerCase(Locale.ENGLISH)); + } + + // below here be scary parsing related things + + /** + * Set the value to be associated with the given name, replacing + * any previous association. + * + * @param name the parameter name + * @param value the parameter's value + */ + public void set(String name, String value) { + parameters.put(name.trim().toLowerCase(Locale.ENGLISH), value); + } + + /** + * Remove any value associated with the given name. + * + * @param name the parameter name + */ + public void remove(String name) { + parameters.remove(name.trim().toLowerCase(Locale.ENGLISH)); + } + + /** + * Retrieve an enumeration of all the names in this list. + * + * @return an enumeration of all parameter names + */ + public Enumeration getNames() { + return parameters.keys(); + } + + /** + * Return a string representation of this object. + */ + public String toString() { + StringBuilder buffer = new StringBuilder(); + buffer.ensureCapacity(parameters.size() * 16); + // heuristic: 8 characters per field + + Enumeration keys = parameters.keys(); + while (keys.hasMoreElements()) { + String key = keys.nextElement(); + buffer.append("; "); + buffer.append(key); + buffer.append('='); + buffer.append(quote(parameters.get(key))); + } + + return buffer.toString(); + } +} diff --git a/net-mail/src/main/java/jakarta/activation/MimeTypeParseException.java b/net-mail/src/main/java/jakarta/activation/MimeTypeParseException.java new file mode 100644 index 0000000..8384242 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/MimeTypeParseException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +/** + * A class to encapsulate MimeType parsing related exceptions. + */ +@SuppressWarnings("serial") +public class MimeTypeParseException extends Exception { + + /** + * Constructs a MimeTypeParseException with no specified detail message. + */ + public MimeTypeParseException() { + super(); + } + + /** + * Constructs a MimeTypeParseException with the specified detail message. + * + * @param s the detail message. + */ + public MimeTypeParseException(String s) { + super(s); + } +} diff --git a/net-mail/src/main/java/jakarta/activation/MimeTypeRegistry.java b/net-mail/src/main/java/jakarta/activation/MimeTypeRegistry.java new file mode 100644 index 0000000..4ae3c65 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/MimeTypeRegistry.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 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 jakarta.activation; + +/** + * The MimeTypeRegistry interface is implemented by objects that can + * be used to store and retrieve MimeTypeEntries. + *

+ * Application must implement {@link jakarta.activation.spi.MimeTypeRegistryProvider} + * to create new instances of the MimeTypeRegistry. Implementation of the MimeTypeRegistry + * can store MimeTypeEntries in different ways and that storage must be accessible through the + * {@link jakarta.activation.spi.MimeTypeRegistryProvider} methods. + * Implementation of the MimeTypeRegistry must contain in-memory storage for MimeTypeEntries. + */ +public interface MimeTypeRegistry { + + /** + * get the MimeTypeEntry based on the file extension + * + * @param file_ext the file extension + * @return the MimeTypeEntry + */ + MimeTypeEntry getMimeTypeEntry(String file_ext); + + /** + * Get the MIME type string corresponding to the file extension. + * + * @param file_ext the file extension + * @return the MIME type string + */ + default String getMIMETypeString(String file_ext) { + MimeTypeEntry entry = this.getMimeTypeEntry(file_ext); + + if (entry != null) { + return entry.getMIMEType(); + } + return null; + } + + /** + * Appends string of entries to the types registry + * + * @param mime_types the mime.types string + */ + void appendToRegistry(String mime_types); +} \ No newline at end of file diff --git a/net-mail/src/main/java/jakarta/activation/MimetypesFileTypeMap.java b/net-mail/src/main/java/jakarta/activation/MimetypesFileTypeMap.java new file mode 100644 index 0000000..410f6a2 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/MimetypesFileTypeMap.java @@ -0,0 +1,350 @@ +/* + * Copyright (c) 1997, 2024 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 jakarta.activation; + +import jakarta.activation.spi.MimeTypeRegistryProvider; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.nio.file.Path; +import java.util.NoSuchElementException; +import java.util.ServiceConfigurationError; +import java.util.Vector; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * This class extends FileTypeMap and provides data typing of files + * via their file extension. It uses the .mime.types format.

+ * + * MIME types file search order:

+ * The MimetypesFileTypeMap looks in various places in the user's + * system for MIME types file entries. When requests are made + * to search for MIME types in the MimetypesFileTypeMap, it searches + * MIME types files in the following order: + *

    + *
  1. Programmatically added entries to the MimetypesFileTypeMap instance. + *
  2. The file .mime.types in the user's home directory. + *
  3. The file mime.types in the Java runtime. + *
  4. The file or resources named META-INF/mime.types. + *
  5. The file or resource named META-INF/mimetypes.default + * (usually found only in the activation.jar file). + *
+ *

+ * (The current implementation looks for the mime.types file + * in the Java runtime in the directory java.home/conf + * if it exists, and otherwise in the directory + * java.home/lib, where java.home is the value + * of the "java.home" System property. Note that the "conf" directory was + * introduced in JDK 9.) + *

+ * MIME types file format:

+ * + * + * # comments begin with a '#'
+ * # the format is <mime type> <space separated file extensions>
+ * # for example:
+ * text/plain txt text TXT
+ * # this would map file.txt, file.text, and file.TXT to
+ * # the mime type "text/plain"
+ *
+ * + * @author Bart Calder + * @author Bill Shannon + */ +public class MimetypesFileTypeMap extends FileTypeMap { + + private static final Logger logger = Logger.getLogger(MimetypesFileTypeMap.class.getName()); + + private static final int PROG = 0; + private static final String defaultType = "application/octet-stream"; + private static final String confDir; + + static { + String dir; + String home = System.getProperty("java.home"); + String newdir = home + File.separator + "conf"; + File conf = new File(newdir); + if (conf.exists()) { + dir = newdir + File.separator; + } else { + dir = home + File.separator + "lib" + File.separator; + } + confDir = dir; + } + + private MimeTypeRegistry[] DB; + + /** + * The default constructor. + */ + public MimetypesFileTypeMap() { + Vector dbv = new Vector<>(5); + MimeTypeRegistry mf; + dbv.addElement(null); + logger.log(Level.FINE, "MimetypesFileTypeMap: load HOME"); + String user_home = System.getProperty("user.home"); + if (user_home != null) { + String path = user_home + File.separator + ".mime.types"; + mf = loadFile(path); + if (mf != null) + dbv.addElement(mf); + } + logger.log(Level.FINE, "MimetypesFileTypeMap: load SYS"); + // check system's home + if (confDir != null) { + mf = loadFile(confDir + "mime.types"); + if (mf != null) + dbv.addElement(mf); + } + logger.log(Level.FINE, "MimetypesFileTypeMap: load JAR"); + // load from the app's jar file + loadAllResources(dbv, "META-INF/mime.types"); + + logger.log(Level.FINE, "MimetypesFileTypeMap: load DEF"); + mf = loadResource("/META-INF/mimetypes.default"); + + if (mf != null) + dbv.addElement(mf); + + DB = new MimeTypeRegistry[dbv.size()]; + dbv.copyInto(DB); + } + + /** + * Construct a MimetypesFileTypeMap with programmatic entries + * added from the named file. + * + * @param mimeTypeFileName the file name + * @throws IOException for errors reading the file + */ + public MimetypesFileTypeMap(String mimeTypeFileName) throws IOException { + this(); + try { + DB[PROG] = getImplementation().getByFileName(mimeTypeFileName); + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + String errorMessage = "Cannot find or load an implementation for MimeTypeRegistryProvider." + + "MimeTypeRegistry: can't load " + mimeTypeFileName; + logger.log(Level.FINE, errorMessage, e); + throw new IOException(errorMessage, e); + } + } + + /** + * Construct a MimetypesFileTypeMap with programmatic entries + * added from the InputStream. + * + * @param is the input stream to read from + */ + public MimetypesFileTypeMap(InputStream is) { + this(); + try { + DB[PROG] = getImplementation().getByInputStream(is); + } catch (IOException ex) { + // XXX - really should throw it + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + logger.log(Level.FINE, "Cannot find or load an implementation for MimeTypeRegistryProvider." + + "MimeTypeRegistry: can't load InputStream", e); + } + } + + /** + * Load from the named resource. + */ + private MimeTypeRegistry loadResource(String name) { + InputStream clis = null; + try { + clis = Util.getResourceAsStream(this.getClass(), name); + if (clis != null) { + MimeTypeRegistry mf = getImplementation().getByInputStream(clis); + logger.log(Level.FINE, "MimetypesFileTypeMap: successfully " + + "loaded mime types file: " + name); + return mf; + } else { + logger.log(Level.FINE, "MimetypesFileTypeMap: not loading " + + "mime types file: " + name); + } + } catch (IOException e) { + logger.log(Level.FINE, "MimetypesFileTypeMap: can't load " + name, e); + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + logger.log(Level.FINE, "Cannot find or load an implementation for MimeTypeRegistryProvider." + + "MimeTypeRegistry: can't load " + name, e); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException ex) { + logger.log(Level.FINE, "InputStream cannot be close for " + name, ex); + } + } + return null; + } + + /** + * Load all of the named resource. + */ + private void loadAllResources(Vector v, String name) { + boolean anyLoaded = false; + try { + URL[] urls; + ClassLoader cld = null; + // First try the "application's" class loader. + cld = Util.getContextClassLoader(); + if (cld == null) + cld = this.getClass().getClassLoader(); + if (cld != null) + urls = Util.getResources(cld, name); + else + urls = Util.getSystemResources(name); + if (urls != null) { + logger.log(Level.FINE, "MimetypesFileTypeMap: getResources"); + for (int i = 0; i < urls.length; i++) { + URL url = urls[i]; + InputStream clis = null; + logger.log(Level.FINE, "MimetypesFileTypeMap: URL " + url); + try { + clis = Util.openStream(url); + if (clis != null) { + v.addElement( + getImplementation().getByInputStream(clis) + ); + anyLoaded = true; + logger.log(Level.FINE, "MimetypesFileTypeMap: " + + "successfully loaded " + + "mime types from URL: " + url); + } else { + logger.log(Level.FINE, "MimetypesFileTypeMap: " + + "not loading " + + "mime types from URL: " + url); + } + } catch (IOException ioex) { + logger.log(Level.FINE, "MimetypesFileTypeMap: can't load " + + url, ioex); + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + logger.log(Level.FINE, "Cannot find or load an implementation for MimeTypeRegistryProvider." + + "MimeTypeRegistry: can't load " + url, e); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException cex) { + logger.log(Level.FINE, "InputStream cannot be close for " + name, cex); + } + } + } + } + } catch (Exception ex) { + logger.log(Level.FINE, "MimetypesFileTypeMap: can't load " + name, ex); + } + + // if failed to load anything, fall back to old technique, just in case + if (!anyLoaded) { + logger.log(Level.FINE, "MimetypesFileTypeMap: !anyLoaded"); + MimeTypeRegistry mf = loadResource("/" + name); + if (mf != null) + v.addElement(mf); + } + } + + /** + * Load the named file. + */ + private MimeTypeRegistry loadFile(String name) { + MimeTypeRegistry mtf = null; + + try { + mtf = getImplementation().getByFileName(name); + } catch (IOException e) { + logger.log(Level.FINE, "MimeTypeRegistry: can't load from file - " + name, e); + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + logger.log(Level.FINE, "Cannot find or load an implementation for MimeTypeRegistryProvider." + + "MimeTypeRegistry: can't load " + name, e); + } + return mtf; + } + + /** + * Prepend the MIME type values to the registry. + * + * @param mime_types A .mime.types formatted string of entries. + */ + public synchronized void addMimeTypes(String mime_types) { + try { + if (DB[PROG] == null) { + DB[PROG] = getImplementation().getInMemory(); + } + DB[PROG].appendToRegistry(mime_types); + } catch (NoSuchElementException | IllegalStateException | ServiceConfigurationError e) { + logger.log(Level.FINE, "Cannot find or load an implementation for MimeTypeRegistryProvider." + + "MimeTypeRegistry: can't add " + mime_types, e); + throw e; + } + } + + /** + * Return the MIME type of the File object. + * The implementation in this class calls + * getContentType(f.getName()). + * + * @param f the file + * @return the file's MIME type + */ + public String getContentType(File f) { + return this.getContentType(f.getName()); + } + + /** + * Return the MIME type of the Path object. + * The implementation in this class calls + * getContentType(p.getFileName().toString()). + * + * @param p the file Path + * @return the file's MIME type + */ + public String getContentType(Path p) { + return this.getContentType(p.getFileName().toString()); + } + + /** + * Return the MIME type based on the specified file name. + * The MIME type entries are searched as described above under + * MIME types file search order. + * If no entry is found, the type "application/octet-stream" is returned. + * + * @param filename the file name + * @return the file's MIME type + */ + public synchronized String getContentType(String filename) { + int dot_pos = filename.lastIndexOf("."); // period index + if (dot_pos < 0) { + return defaultType; + } + String file_ext = filename.substring(dot_pos + 1); + if (file_ext.isEmpty()) { + return defaultType; + } + for (MimeTypeRegistry mimeTypeRegistry : DB) { + if (mimeTypeRegistry == null) { + continue; + } + String result = mimeTypeRegistry.getMIMETypeString(file_ext); + if (result != null) { + return result; + } + } + return defaultType; + } + + private MimeTypeRegistryProvider getImplementation() { + return FactoryFinder.find(MimeTypeRegistryProvider.class); + } +} diff --git a/net-mail/src/main/java/jakarta/activation/ServiceLoaderUtil.java b/net-mail/src/main/java/jakarta/activation/ServiceLoaderUtil.java new file mode 100644 index 0000000..1c0f38d --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/ServiceLoaderUtil.java @@ -0,0 +1,74 @@ +/* + * Copyright (c) 2021, 2024 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 jakarta.activation; + +import java.util.ServiceLoader; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Shared ServiceLoader/FactoryFinder Utils. JAF and MAIL use the same loading + * logic of thread context class loader, calling class loader, and finally the + * system class loader. + * + * @author Miroslav.Kos@oracle.com + */ +class ServiceLoaderUtil { + + static P firstByServiceLoader(Class

spiClass, + ClassLoader loader, + Logger logger, + ExceptionHandler handler) throws T { + logger.log(Level.FINE, "Using java.util.ServiceLoader to find {0}", spiClass.getName()); + try { + ServiceLoader

serviceLoader = ServiceLoader.load(spiClass, loader); + for (P impl : serviceLoader) { + logger.log(Level.FINE, "ServiceProvider loading Facility used; returning object [{0}]", impl.getClass().getName()); + return impl; + } + } catch (Throwable t) { + throw handler.createException(t, "Error while searching for service [" + spiClass.getName() + "]"); + } + return null; + } + + @SuppressWarnings({"unchecked"}) + static

Class

nullSafeLoadClass(String className, ClassLoader classLoader) throws ClassNotFoundException { + if (classLoader == null) { + classLoader = ClassLoader.getSystemClassLoader(); + } + return (Class

) Class.forName(className, false, classLoader); + } + + static P newInstance(String className, + Class

service, ClassLoader classLoader, + final ExceptionHandler handler) throws T { + try { + Class

cls = safeLoadClass(className, classLoader); + return service.cast(cls.getConstructor().newInstance()); + } catch (ClassNotFoundException x) { + throw handler.createException(x, "Provider " + className + " not found"); + } catch (Exception x) { + throw handler.createException(x, "Provider " + className + " could not be instantiated: " + x); + } + } + + static

Class

safeLoadClass(String className, + ClassLoader classLoader) throws ClassNotFoundException { + return nullSafeLoadClass(className, classLoader); + } + + static abstract class ExceptionHandler { + + public abstract T createException(Throwable throwable, String message); + + } +} diff --git a/net-mail/src/main/java/jakarta/activation/URLDataSource.java b/net-mail/src/main/java/jakarta/activation/URLDataSource.java new file mode 100644 index 0000000..e2ce8c5 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/URLDataSource.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.net.URLConnection; + +/** + * The URLDataSource class provides an object that wraps a URL + * object in a DataSource interface. URLDataSource simplifies the handling + * of data described by URLs within Jakarta Activation + * because this class can be used to create new DataHandlers. NOTE: The + * DataHandler object creates a URLDataSource internally, + * when it is constructed with a URL. + * + * @see DataSource + * @see DataHandler + */ +public class URLDataSource implements DataSource { + private URL url = null; + private URLConnection url_conn = null; + + /** + * URLDataSource constructor. The URLDataSource class will + * not open a connection to the URL until a method requiring it + * to do so is called. + * + * @param url The URL to be encapsulated in this object. + */ + public URLDataSource(URL url) { + this.url = url; + } + + /** + * Returns the value of the URL content-type header field. + * It calls the URL's URLConnection.getContentType method + * after retrieving a URLConnection object. + * Note: this method attempts to call the openConnection + * method on the URL. If this method fails, or if a content type is not + * returned from the URLConnection, getContentType returns + * "application/octet-stream" as the content type. + * + * @return the content type. + */ + public String getContentType() { + String type = null; + + try { + if (url_conn == null) + url_conn = url.openConnection(); + } catch (IOException e) { + } + + if (url_conn != null) + type = url_conn.getContentType(); + + if (type == null) + type = "application/octet-stream"; + + return type; + } + + /** + * Calls the getFile method on the URL used to + * instantiate the object. + * + * @return the result of calling the URL's getFile method. + */ + public String getName() { + return url.getFile(); + } + + /** + * The getInputStream method from the URL. Calls the + * openStream method on the URL. + * + * @return the InputStream. + */ + public InputStream getInputStream() throws IOException { + return url.openStream(); + } + + /** + * The getOutputStream method from the URL. First an attempt is + * made to get the URLConnection object for the URL. If that + * succeeds, the getOutputStream method on the URLConnection + * is returned. + * + * @return the OutputStream. + */ + public OutputStream getOutputStream() throws IOException { + // get the url connection if it is available + url_conn = url.openConnection(); + + if (url_conn != null) { + url_conn.setDoOutput(true); + return url_conn.getOutputStream(); + } else + return null; + } + + /** + * Return the URL used to create this DataSource. + * + * @return The URL. + */ + public URL getURL() { + return url; + } +} diff --git a/net-mail/src/main/java/jakarta/activation/UnsupportedDataTypeException.java b/net-mail/src/main/java/jakarta/activation/UnsupportedDataTypeException.java new file mode 100644 index 0000000..bd0ea0d --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/UnsupportedDataTypeException.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +import java.io.IOException; + +/** + * Signals that the requested operation does not support the + * requested data type. + * + * @see DataHandler + */ +@SuppressWarnings("serial") +public class UnsupportedDataTypeException extends IOException { + + /** + * Constructs an UnsupportedDataTypeException with no detail + * message. + */ + public UnsupportedDataTypeException() { + super(); + } + + /** + * Constructs an UnsupportedDataTypeException with the specified + * message. + * + * @param s The detail message. + */ + public UnsupportedDataTypeException(String s) { + super(s); + } +} diff --git a/net-mail/src/main/java/jakarta/activation/Util.java b/net-mail/src/main/java/jakarta/activation/Util.java new file mode 100644 index 0000000..79b7d39 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/Util.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.activation; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.List; + +class Util { + + private Util() { + // private constructor, can't create an instance + } + + public static ClassLoader getContextClassLoader() { + return Thread.currentThread().getContextClassLoader(); + } + + public static InputStream getResourceAsStream(final Class c, + final String name) throws IOException { + return c.getResourceAsStream(name); + } + + public static URL[] getResources(final ClassLoader cl, final String name) { + URL[] ret = null; + try { + List v = new ArrayList<>(); + Enumeration e = cl.getResources(name); + while (e != null && e.hasMoreElements()) { + URL url = e.nextElement(); + if (url != null) + v.add(url); + } + if (!v.isEmpty()) { + ret = new URL[v.size()]; + ret = v.toArray(ret); + } + } catch (IOException ioex) { + // + } + return ret; + } + + public static URL[] getSystemResources(final String name) { + URL[] ret = null; + try { + List v = new ArrayList<>(); + Enumeration e = ClassLoader.getSystemResources(name); + while (e != null && e.hasMoreElements()) { + URL url = e.nextElement(); + if (url != null) + v.add(url); + } + if (!v.isEmpty()) { + ret = new URL[v.size()]; + ret = v.toArray(ret); + } + } catch (IOException ioex) { + // + } + return ret; + } + + public static InputStream openStream(final URL url) throws IOException { + return url.openStream(); + } +} diff --git a/net-mail/src/main/java/jakarta/activation/package-info.java b/net-mail/src/main/java/jakarta/activation/package-info.java new file mode 100644 index 0000000..2e26440 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/package-info.java @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * Jakarta Activation is used by Jakarta Mail to manage MIME data. + */ +package jakarta.activation; \ No newline at end of file diff --git a/net-mail/src/main/java/jakarta/activation/spi/MailcapRegistryProvider.java b/net-mail/src/main/java/jakarta/activation/spi/MailcapRegistryProvider.java new file mode 100644 index 0000000..4dd02d7 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/spi/MailcapRegistryProvider.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2021 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 jakarta.activation.spi; + +import jakarta.activation.MailcapRegistry; +import java.io.IOException; +import java.io.InputStream; +import java.util.NoSuchElementException; +import java.util.ServiceConfigurationError; + +/** + * This interface defines a factory for MailcapRegistry. An + * implementation of this interface should provide instances of the MailcapRegistry + * based on the way how to access the storage for MailcapEntries. + *

+ * Jakarta Activation uses Service Provider Interface and ServiceLoader + * to obtain an instance of the implementation of the MailcapRegistryProvider. + */ +public interface MailcapRegistryProvider { + + /** + * Retrieve an instance of the MailcapRegistry based on the name of the file where the MailcapEntries are stored. + * + * @param name The name of the file that stores MailcapEntries. + * @return The instance of the MailcapRegistry, or null if none are found. + * @throws IOException If an instance of the MailcapRegistry class cannot be found or loaded. + */ + MailcapRegistry getByFileName(String name) throws IOException; + + /** + * Retrieve an instance of the MailcapRegistry based on the InputStream + * that is used to read data from some named resource. + * + * @param inputStream InputStream for some resource that contains MailcapEntries. + * @return The instance of the MailcapRegistry, or null if none are found. + * @throws IOException If an instance of the MailcapRegistry class cannot be found or loaded. + */ + MailcapRegistry getByInputStream(InputStream inputStream) throws IOException; + + /** + * Retrieve an instance of the in-memory implementation of the MailcapRegistry. + * + * @return In-memory implementation of the MailcapRegistry. + * @throws NoSuchElementException If no implementations were found. + * @throws ServiceConfigurationError If no implementations were loaded. + */ + MailcapRegistry getInMemory(); +} diff --git a/net-mail/src/main/java/jakarta/activation/spi/MimeTypeRegistryProvider.java b/net-mail/src/main/java/jakarta/activation/spi/MimeTypeRegistryProvider.java new file mode 100644 index 0000000..700e818 --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/spi/MimeTypeRegistryProvider.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2021 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 jakarta.activation.spi; + +import jakarta.activation.MimeTypeRegistry; +import java.io.IOException; +import java.io.InputStream; +import java.util.NoSuchElementException; +import java.util.ServiceConfigurationError; + +/** + * This interface defines a factory for MimeTypeRegistry. An + * implementation of this interface should provide instances of the MimeTypeRegistry + * based on the way how to access the storage for MimeTypeEntries. + *

+ * Jakarta Activation uses Service Provider Interface and ServiceLoader + * to obtain an instance of the implementation of the MimeTypeRegistryProvider. + */ +public interface MimeTypeRegistryProvider { + + /** + * Retrieve an instance of the MimeTypeRegistry based on the name of the file where the MimeTypeEntries are stored. + * + * @param name The name of the file that stores MimeTypeEntries. + * @return The instance of the MimeTypeRegistry, or null if none are found. + * @throws IOException If an instance of the MailcapRegistry class cannot be found or loaded. + */ + MimeTypeRegistry getByFileName(String name) throws IOException; + + /** + * Retrieve an instance of the MimeTypeRegistry based on the InputStream + * that is used to read data from some named resource. + * + * @param inputStream InputStream for some resource that contains MimeTypeEntries. + * @return The instance of the MimeTypeRegistry, or null if none are found. + * @throws IOException If an instance of the MailcapRegistry class cannot be found or loaded. + */ + MimeTypeRegistry getByInputStream(InputStream inputStream) throws IOException; + + /** + * Retrieve an instance of the in-memory implementation of the MimeTypeRegistry. + * Jakarta Activation can throw NoSuchElementException or ServiceConfigurationError + * if no implementations were found. + * + * @return In-memory implementation of the MimeTypeRegistry. + * @throws NoSuchElementException If no implementations were found. + * @throws ServiceConfigurationError If no implementations were loaded. + */ + MimeTypeRegistry getInMemory(); +} \ No newline at end of file diff --git a/net-mail/src/main/java/jakarta/activation/spi/package-info.java b/net-mail/src/main/java/jakarta/activation/spi/package-info.java new file mode 100644 index 0000000..59488ad --- /dev/null +++ b/net-mail/src/main/java/jakarta/activation/spi/package-info.java @@ -0,0 +1,18 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + *

Provides interfaces which implementations will be used as service providers for other services + * that used by Jakarta Activation.

+ *

Implementation of Jakarta Activation must implement interfaces declared in this package. + * Jakarta Activation uses {@link java.util.ServiceLoader} class to discover + * and load implementations of the interfaces from this package using standard Java SPI mechanism.

+ */ +package jakarta.activation.spi; \ No newline at end of file diff --git a/net-mail/src/main/java/jakarta/mail/Address.java b/net-mail/src/main/java/jakarta/mail/Address.java new file mode 100644 index 0000000..048beb1 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Address.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * This abstract class models the addresses in a message. + * Subclasses provide specific implementations. Subclasses + * will typically be serializable so that (for example) the + * use of Address objects in search terms can be serialized + * along with the search terms. + * + * @author John Mani + * @author Bill Shannon + */ + +public abstract class Address { + + /** + * Creates a default {@code Address}. + */ + public Address() { + } + + /** + * Return a type string that identifies this address type. + * + * @return address type + * @see jakarta.mail.internet.InternetAddress + */ + public abstract String getType(); + + /** + * Return a String representation of this address object. + * + * @return string representation of this address + */ + @Override + public abstract String toString(); + + /** + * The equality operator. Subclasses should provide an + * implementation of this method that supports value equality + * (do the two Address objects represent the same destination?), + * not object reference equality. A subclass must also provide + * a corresponding implementation of the hashCode + * method that preserves the general contract of + * equals and hashCode - objects that + * compare as equal must have the same hashCode. + * + * @param address Address object + */ + @Override + public abstract boolean equals(Object address); +} diff --git a/net-mail/src/main/java/jakarta/mail/AuthenticationFailedException.java b/net-mail/src/main/java/jakarta/mail/AuthenticationFailedException.java new file mode 100644 index 0000000..6527694 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/AuthenticationFailedException.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * This exception is thrown when the connect method on a Store or + * Transport object fails due to an authentication failure (e.g., + * bad user name or password). + * + * @author Bill Shannon + */ +@SuppressWarnings("serial") +public class AuthenticationFailedException extends MessagingException { + + /** + * Constructs an AuthenticationFailedException. + */ + public AuthenticationFailedException() { + super(); + } + + /** + * Constructs an AuthenticationFailedException with the specified + * detail message. + * + * @param message The detailed error message + */ + public AuthenticationFailedException(String message) { + super(message); + } + + /** + * Constructs an AuthenticationFailedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public AuthenticationFailedException(String message, Exception e) { + super(message, e); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/Authenticator.java b/net-mail/src/main/java/jakarta/mail/Authenticator.java new file mode 100644 index 0000000..8a7e8a1 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Authenticator.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import java.net.InetAddress; + +/** + * The class Authenticator represents an object that knows how to obtain + * authentication for a network connection. Usually, it will do this + * by prompting the user for information. + *

+ * Applications use this class by creating a subclass, and registering + * an instance of that subclass with the session when it is created. + * When authentication is required, the system will invoke a method + * on the subclass (like getPasswordAuthentication). The subclass's + * method can query about the authentication being requested with a + * number of inherited methods (getRequestingXXX()), and form an + * appropriate message for the user. + *

+ * All methods that request authentication have a default implementation + * that fails. + * + * @author Bill Foote + * @author Bill Shannon + * @see java.net.Authenticator + * @see Session#getInstance(java.util.Properties, + * Authenticator) + * @see Session#getDefaultInstance(java.util.Properties, + * Authenticator) + * @see Session#requestPasswordAuthentication + * @see PasswordAuthentication + */ +public abstract class Authenticator { + + private InetAddress requestingSite; + private int requestingPort; + private String requestingProtocol; + private String requestingPrompt; + private String requestingUserName; + + /** + * Creates a default {@code Authenticator}. + * There are no abstract methods, but to be useful the user must subclass. + * + * @see #getPasswordAuthentication() + */ + public Authenticator() { + } + + /** + * Ask the authenticator for a password. + *

+ * + * @param addr The InetAddress of the site requesting authorization, + * or null if not known. + * @param port the port for the requested connection + * @param protocol The protocol that's requesting the connection + * (@see java.net.Authenticator.getProtocol()) + * @param prompt A prompt string for the user + * @return The username/password, or null if one can't be gotten. + */ + final synchronized PasswordAuthentication requestPasswordAuthentication( + InetAddress addr, int port, String protocol, + String prompt, String defaultUserName) { + requestingSite = addr; + requestingPort = port; + requestingProtocol = protocol; + requestingPrompt = prompt; + requestingUserName = defaultUserName; + return getPasswordAuthentication(); + } + + /** + * @return the InetAddress of the site requesting authorization, or null + * if it's not available. + */ + protected final InetAddress getRequestingSite() { + return requestingSite; + } + + /** + * @return the port for the requested connection + */ + protected final int getRequestingPort() { + return requestingPort; + } + + /** + * Give the protocol that's requesting the connection. Often this + * will be based on a URLName. + * + * @return the protcol + * @see URLName#getProtocol + */ + protected final String getRequestingProtocol() { + return requestingProtocol; + } + + /** + * @return the prompt string given by the requestor + */ + protected final String getRequestingPrompt() { + return requestingPrompt; + } + + /** + * @return the default user name given by the requestor + */ + protected final String getDefaultUserName() { + return requestingUserName; + } + + /** + * Called when password authentication is needed. Subclasses should + * override the default implementation, which returns null.

+ *

+ * Note that if this method uses a dialog to prompt the user for this + * information, the dialog needs to block until the user supplies the + * information. This method can not simply return after showing the + * dialog. + * + * @return The PasswordAuthentication collected from the + * user, or null if none is provided. + */ + protected PasswordAuthentication getPasswordAuthentication() { + return null; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/BodyPart.java b/net-mail/src/main/java/jakarta/mail/BodyPart.java new file mode 100644 index 0000000..9cbd2e0 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/BodyPart.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import jakarta.mail.util.StreamProvider; + +/** + * This class models a Part that is contained within a Multipart. + * This is an abstract class. Subclasses provide actual implementations.

+ *

+ * BodyPart implements the Part interface. Thus, it contains a set of + * attributes and a "content". + * + * @author John Mani + * @author Bill Shannon + */ + +public abstract class BodyPart implements Part { + + /** + * Instance of stream provider. + * + * @since JavaMail 2.1 + */ + protected final StreamProvider streamProvider = StreamProvider.provider(); + /** + * The Multipart object containing this BodyPart, + * if known. + * + * @since JavaMail 1.1 + */ + protected Multipart parent; + + /** + * Creates a default {@code BodyPart}. + */ + public BodyPart() { + } + + /** + * Return the containing Multipart object, + * or null if not known. + * + * @return the parent Multipart + */ + public Multipart getParent() { + return parent; + } + + /** + * Set the parent of this BodyPart to be the specified + * Multipart. Normally called by Multipart's + * addBodyPart method. parent may be + * null if the BodyPart is being removed + * from its containing Multipart. + * + * @since JavaMail 1.1 + */ + void setParent(Multipart parent) { + this.parent = parent; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/EncodingAware.java b/net-mail/src/main/java/jakarta/mail/EncodingAware.java new file mode 100644 index 0000000..2d3697c --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/EncodingAware.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * A {@link jakarta.activation.DataSource DataSource} that also implements + * EncodingAware may specify the Content-Transfer-Encoding + * to use for its data. Valid Content-Transfer-Encoding values specified + * by RFC 2045 are "7bit", "8bit", "quoted-printable", "base64", and "binary". + *

+ * For example, a {@link jakarta.activation.FileDataSource FileDataSource} + * could be created that forces all files to be base64 encoded: + *

+ *  public class Base64FileDataSource extends FileDataSource
+ * 					implements EncodingAware {
+ * 	public Base64FileDataSource(File file) {
+ * 	    super(file);
+ *    }
+ *
+ * 	// implements EncodingAware.getEncoding()
+ * 	public String getEncoding() {
+ * 	    return "base64";
+ *    }
+ *  }
+ * 
+ * + * @author Bill Shannon + * @since JavaMail 1.5 + */ + +public interface EncodingAware { + + /** + * Return the MIME Content-Transfer-Encoding to use for this data, + * or null to indicate that an appropriate value should be chosen + * by the caller. + * + * @return the Content-Transfer-Encoding value, or null + */ + String getEncoding(); +} diff --git a/net-mail/src/main/java/jakarta/mail/EventQueue.java b/net-mail/src/main/java/jakarta/mail/EventQueue.java new file mode 100644 index 0000000..30bbbe4 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/EventQueue.java @@ -0,0 +1,159 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import jakarta.mail.event.MailEvent; +import java.util.EventListener; +import java.util.Vector; +import java.util.WeakHashMap; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingQueue; + +/** + * Package private class used by Store & Folder to dispatch events. + * This class implements an event queue, and a dispatcher thread that + * dequeues and dispatches events from the queue. + * + * @author Bill Shannon + */ +class EventQueue implements Runnable { + + private static WeakHashMap appq; + private volatile BlockingQueue q; + private Executor executor; + + /** + * Construct an EventQueue using the specified Executor. + * If the Executor is null, threads will be created as needed. + */ + EventQueue(Executor ex) { + this.executor = ex; + } + + /** + * Create (if necessary) an application-scoped event queue. + * Application scoping is based on the thread's context class loader. + */ + static synchronized EventQueue getApplicationEventQueue(Executor ex) { + ClassLoader cl = Thread.currentThread().getContextClassLoader(); + if (appq == null) + appq = new WeakHashMap<>(); + EventQueue q = appq.get(cl); + if (q == null) { + q = new EventQueue(ex); + appq.put(cl, q); + } + return q; + } + + /** + * Enqueue an event. + */ + synchronized void enqueue(MailEvent event, + Vector vector) { + // if this is the first event, create the queue and start the event task + if (q == null) { + q = new LinkedBlockingQueue<>(); + if (executor != null) { + executor.execute(this); + } else { + Thread qThread = new Thread(this, "Jakarta-Mail-EventQueue"); + qThread.setDaemon(true); // not a user thread + qThread.start(); + } + } + q.add(new QueueElement(event, vector)); + } + + /** + * Terminate the task running the queue, but only if there is a queue. + */ + synchronized void terminateQueue() { + if (q != null) { + Vector dummyListeners = new Vector<>(); + dummyListeners.setSize(1); // need atleast one listener + q.add(new QueueElement(new TerminatorEvent(), dummyListeners)); + q = null; + } + } + + /** + * Pull events off the queue and dispatch them. + */ + @Override + public void run() { + + BlockingQueue bq = q; + if (bq == null) + return; + try { + loop: + for (; ; ) { + // block until an item is available + QueueElement qe = bq.take(); + MailEvent e = qe.event; + Vector v = qe.vector; + + for (int i = 0; i < v.size(); i++) + try { + e.dispatch(v.elementAt(i)); + } catch (Throwable t) { + if (t instanceof InterruptedException) + break loop; + // ignore anything else thrown by the listener + } + + qe = null; + e = null; + v = null; + } + } catch (InterruptedException e) { + // just die + } + } + + /** + * A special event that causes the queue processing task to terminate. + */ + @SuppressWarnings("serial") + static class TerminatorEvent extends MailEvent { + + TerminatorEvent() { + super(new Object()); + } + + @Override + public void dispatch(Object listener) { + // Kill the event dispatching thread. + Thread.currentThread().interrupt(); + } + } + + /** + * A "struct" to put on the queue. + */ + static class QueueElement { + MailEvent event; + Vector vector; + + QueueElement(MailEvent event, Vector vector) { + this.event = event; + this.vector = vector; + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/FetchProfile.java b/net-mail/src/main/java/jakarta/mail/FetchProfile.java new file mode 100644 index 0000000..10d98c0 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/FetchProfile.java @@ -0,0 +1,223 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import java.util.Vector; + +/** + * Clients use a FetchProfile to list the Message attributes that + * it wishes to prefetch from the server for a range of messages.

+ *

+ * Messages obtained from a Folder are light-weight objects that + * typically start off as empty references to the actual messages. + * Such a Message object is filled in "on-demand" when the appropriate + * get*() methods are invoked on that particular Message. Certain + * server-based message access protocols (Ex: IMAP) allow batch + * fetching of message attributes for a range of messages in a single + * request. Clients that want to use message attributes for a range of + * Messages (Example: to display the top-level headers in a headerlist) + * might want to use the optimization provided by such servers. The + * FetchProfile allows the client to indicate this desire + * to the server.

+ *

+ * Note that implementations are not obligated to support + * FetchProfiles, since there might be cases where the backend service + * does not allow easy, efficient fetching of such profiles.

+ *

+ * Sample code that illustrates the use of a FetchProfile is given + * below: + *

+ *
+ *
+ *  Message[] msgs = folder.getMessages();
+ *
+ *  FetchProfile fp = new FetchProfile();
+ *  fp.add(FetchProfile.Item.ENVELOPE);
+ *  fp.add("X-mailer");
+ *  folder.fetch(msgs, fp);
+ *
+ * 
+ * + * @author John Mani + * @author Bill Shannon + * @see Folder#fetch + */ + +public class FetchProfile { + + private Vector specials; // specials + private Vector headers; // vector of header names + + /** + * Create an empty FetchProfile. + */ + public FetchProfile() { + specials = null; + headers = null; + } + + /** + * Add the given special item as one of the attributes to + * be prefetched. + * + * @param item the special item to be fetched + * @see Item#ENVELOPE + * @see Item#CONTENT_INFO + * @see Item#FLAGS + */ + public void add(Item item) { + if (specials == null) + specials = new Vector<>(); + specials.addElement(item); + } + + /** + * Add the specified header-field to the list of attributes + * to be prefetched. + * + * @param headerName header to be prefetched + */ + public void add(String headerName) { + if (headers == null) + headers = new Vector<>(); + headers.addElement(headerName); + } + + /** + * Returns true if the fetch profile contains the given special item. + * + * @param item the Item to test + * @return true if the fetch profile contains the given special item + */ + public boolean contains(Item item) { + return specials != null && specials.contains(item); + } + + /** + * Returns true if the fetch profile contains the given header name. + * + * @param headerName the header to test + * @return true if the fetch profile contains the given header name + */ + public boolean contains(String headerName) { + return headers != null && headers.contains(headerName); + } + + /** + * Get the items set in this profile. + * + * @return items set in this profile + */ + public Item[] getItems() { + if (specials == null) + return new Item[0]; + + Item[] s = new Item[specials.size()]; + specials.copyInto(s); + return s; + } + + /** + * Get the names of the header-fields set in this profile. + * + * @return headers set in this profile + */ + public String[] getHeaderNames() { + if (headers == null) + return new String[0]; + + String[] s = new String[headers.size()]; + headers.copyInto(s); + return s; + } + + /** + * This inner class is the base class of all items that + * can be requested in a FetchProfile. The items currently + * defined here are ENVELOPE, CONTENT_INFO + * and FLAGS. The UIDFolder interface + * defines the UID Item as well.

+ *

+ * Note that this class only has a protected constructor, therby + * restricting new Item types to either this class or subclasses. + * This effectively implements a enumeration of allowed Item types. + * + * @see UIDFolder + */ + + public static class Item { + /** + * This is the Envelope item.

+ *

+ * The Envelope is an aggregration of the common attributes + * of a Message. Implementations should include the following + * attributes: From, To, Cc, Bcc, ReplyTo, Subject and Date. + * More items may be included as well.

+ *

+ * For implementations of the IMAP4 protocol (RFC 2060), the + * Envelope should include the ENVELOPE data item. More items + * may be included too. + */ + public static final Item ENVELOPE = new Item("ENVELOPE"); + + /** + * This item is for fetching information about the + * content of the message.

+ *

+ * This includes all the attributes that describe the content + * of the message. Implementations should include the following + * attributes: ContentType, ContentDisposition, + * ContentDescription, Size and LineCount. Other items may be + * included as well. + */ + public static final Item CONTENT_INFO = new Item("CONTENT_INFO"); + + /** + * 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 should be prefetched. + * + * @since JavaMail 1.5 + */ + public static final Item SIZE = new Item("SIZE"); + + /** + * This is the Flags item. + */ + public static final Item FLAGS = new Item("FLAGS"); + + private String name; + + /** + * Constructor for an item. The name is used only for debugging. + * + * @param name the item name + */ + protected Item(String name) { + this.name = name; + } + + /** + * Include the name in the toString return value for debugging. + */ + @Override + public String toString() { + return getClass().getName() + "[" + name + "]"; + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/Flags.java b/net-mail/src/main/java/jakarta/mail/Flags.java new file mode 100644 index 0000000..5786d91 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Flags.java @@ -0,0 +1,667 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import java.util.Enumeration; +import java.util.Hashtable; +import java.util.Locale; +import java.util.Vector; + +/** + * The Flags class represents the set of flags on a Message. Flags + * are composed of predefined system flags, and user defined flags.

+ *

+ * A System flag is represented by the Flags.Flag + * inner class. A User defined flag is represented as a String. + * User flags are case-independent.

+ *

+ * A set of standard system flags are predefined. Most folder + * implementations are expected to support these flags. Some + * implementations may also support arbitrary user-defined flags. The + * getPermanentFlags method on a Folder returns a Flags + * object that holds all the flags that are supported by that folder + * implementation.

+ *

+ * A Flags object is serializable so that (for example) the + * use of Flags objects in search terms can be serialized + * along with the search terms.

+ * + * Warning: + * Serialized objects of this class may not be compatible with future + * Jakarta Mail API releases. The current serialization support is + * appropriate for short term storage.

+ *

+ * The below code sample illustrates how to set, examine, and get the + * flags for a message. + *

+ *
+ * Message m = folder.getMessage(1);
+ * m.setFlag(Flags.Flag.DELETED, true); // set the DELETED flag
+ *
+ * // Check if DELETED flag is set on this message
+ * if (m.isSet(Flags.Flag.DELETED))
+ * 	System.out.println("DELETED message");
+ *
+ * // Examine ALL system flags for this message
+ * Flags flags = m.getFlags();
+ * Flags.Flag[] sf = flags.getSystemFlags();
+ * for (int i = 0; i < sf.length; i++) {
+ * 	if (sf[i] == Flags.Flag.DELETED)
+ *            System.out.println("DELETED message");
+ * 	else if (sf[i] == Flags.Flag.SEEN)
+ *            System.out.println("SEEN message");
+ *      ......
+ *      ......
+ * }
+ * 
+ * + * @author John Mani + * @author Bill Shannon + * @see Folder#getPermanentFlags + */ + +public class Flags { + + private final static int ANSWERED_BIT = 0x01; + private final static int DELETED_BIT = 0x02; + private final static int DRAFT_BIT = 0x04; + private final static int FLAGGED_BIT = 0x08; + private final static int RECENT_BIT = 0x10; + private final static int SEEN_BIT = 0x20; + private final static int USER_BIT = 0x80000000; + private int system_flags = 0; + // used as a case-independent Set that preserves the original case, + // the key is the lowercase flag name and the value is the original + private Hashtable user_flags = null; + + /** + * Construct an empty Flags object. + */ + public Flags() { + } + + + /** + * Construct a Flags object initialized with the given flags. + * + * @param flags the flags for initialization + */ + @SuppressWarnings("unchecked") + public Flags(Flags flags) { + this.system_flags = flags.system_flags; + if (flags.user_flags != null) + this.user_flags = (Hashtable) flags.user_flags.clone(); + } + + /** + * Construct a Flags object initialized with the given system flag. + * + * @param flag the flag for initialization + */ + public Flags(Flag flag) { + this.system_flags |= flag.bit; + } + + /** + * Construct a Flags object initialized with the given user flag. + * + * @param flag the flag for initialization + */ + public Flags(String flag) { + user_flags = new Hashtable<>(1); + user_flags.put(flag.toLowerCase(Locale.ENGLISH), flag); + } + + /** + * Add the specified system flag to this Flags object. + * + * @param flag the flag to add + */ + public void add(Flag flag) { + system_flags |= flag.bit; + } + + /** + * Add the specified user flag to this Flags object. + * + * @param flag the flag to add + */ + public void add(String flag) { + if (user_flags == null) + user_flags = new Hashtable<>(1); + user_flags.put(flag.toLowerCase(Locale.ENGLISH), flag); + } + + /** + * Add all the flags in the given Flags object to this + * Flags object. + * + * @param f Flags object + */ + public void add(Flags f) { + system_flags |= f.system_flags; // add system flags + + if (f.user_flags != null) { // add user-defined flags + if (user_flags == null) + user_flags = new Hashtable<>(1); + + Enumeration e = f.user_flags.keys(); + + while (e.hasMoreElements()) { + String s = e.nextElement(); + user_flags.put(s, f.user_flags.get(s)); + } + } + } + + /** + * Remove the specified system flag from this Flags object. + * + * @param flag the flag to be removed + */ + public void remove(Flag flag) { + system_flags &= ~flag.bit; + } + + /** + * Remove the specified user flag from this Flags object. + * + * @param flag the flag to be removed + */ + public void remove(String flag) { + if (user_flags != null) + user_flags.remove(flag.toLowerCase(Locale.ENGLISH)); + } + + /** + * Remove all flags in the given Flags object from this + * Flags object. + * + * @param f the flag to be removed + */ + public void remove(Flags f) { + system_flags &= ~f.system_flags; // remove system flags + + if (f.user_flags != null) { + if (user_flags == null) + return; + + Enumeration e = f.user_flags.keys(); + while (e.hasMoreElements()) + user_flags.remove(e.nextElement()); + } + } + + /** + * Remove any flags not in the given Flags object. + * Useful for clearing flags not supported by a server. If the + * given Flags object includes the Flags.Flag.USER flag, all user + * flags in this Flags object are retained. + * + * @param f the flags to keep + * @return true if this Flags object changed + * @since JavaMail 1.6 + */ + public boolean retainAll(Flags f) { + boolean changed = false; + int sf = system_flags & f.system_flags; + if (system_flags != sf) { + system_flags = sf; + changed = true; + } + + // if we have user flags, and the USER flag is not set in "f", + // determine which user flags to clear + if (user_flags != null && (f.system_flags & USER_BIT) == 0) { + if (f.user_flags != null) { + Enumeration e = user_flags.keys(); + while (e.hasMoreElements()) { + String key = e.nextElement(); + if (!f.user_flags.containsKey(key)) { + user_flags.remove(key); + changed = true; + } + } + } else { + // if anything in user_flags, throw them away + changed = user_flags.size() > 0; + user_flags = null; + } + } + return changed; + } + + /** + * Check whether the specified system flag is present in this Flags object. + * + * @param flag the flag to test + * @return true of the given flag is present, otherwise false. + */ + public boolean contains(Flag flag) { + return (system_flags & flag.bit) != 0; + } + + /** + * Check whether the specified user flag is present in this Flags object. + * + * @param flag the flag to test + * @return true of the given flag is present, otherwise false. + */ + public boolean contains(String flag) { + if (user_flags == null) + return false; + else + return user_flags.containsKey(flag.toLowerCase(Locale.ENGLISH)); + } + + /** + * Check whether all the flags in the specified Flags object are + * present in this Flags object. + * + * @param f the flags to test + * @return true if all flags in the given Flags object are present, + * otherwise false. + */ + public boolean contains(Flags f) { + // Check system flags + if ((f.system_flags & system_flags) != f.system_flags) + return false; + + // Check user flags + if (f.user_flags != null) { + if (user_flags == null) + return false; + Enumeration e = f.user_flags.keys(); + + while (e.hasMoreElements()) { + if (!user_flags.containsKey(e.nextElement())) + return false; + } + } + + // If we've made it till here, return true + return true; + } + + /** + * Check whether the two Flags objects are equal. + * + * @return true if they're equal + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Flags)) + return false; + + Flags f = (Flags) obj; + + // Check system flags + if (f.system_flags != this.system_flags) + return false; + + // Check user flags + int size = this.user_flags == null ? 0 : this.user_flags.size(); + int fsize = f.user_flags == null ? 0 : f.user_flags.size(); + if (size == 0 && fsize == 0) + return true; + if (f.user_flags != null && this.user_flags != null && fsize == size) + return user_flags.keySet().equals(f.user_flags.keySet()); + + return false; + } + + /** + * Compute a hash code for this Flags object. + * + * @return the hash code + */ + @Override + public int hashCode() { + int hash = system_flags; + if (user_flags != null) { + Enumeration e = user_flags.keys(); + while (e.hasMoreElements()) + hash += e.nextElement().hashCode(); + } + return hash; + } + + /** + * Return all the system flags in this Flags object. Returns + * an array of size zero if no flags are set. + * + * @return array of Flags.Flag objects representing system flags + */ + public Flag[] getSystemFlags() { + Vector v = new Vector<>(); + if ((system_flags & ANSWERED_BIT) != 0) + v.addElement(Flag.ANSWERED); + if ((system_flags & DELETED_BIT) != 0) + v.addElement(Flag.DELETED); + if ((system_flags & DRAFT_BIT) != 0) + v.addElement(Flag.DRAFT); + if ((system_flags & FLAGGED_BIT) != 0) + v.addElement(Flag.FLAGGED); + if ((system_flags & RECENT_BIT) != 0) + v.addElement(Flag.RECENT); + if ((system_flags & SEEN_BIT) != 0) + v.addElement(Flag.SEEN); + if ((system_flags & USER_BIT) != 0) + v.addElement(Flag.USER); + + Flag[] f = new Flag[v.size()]; + v.copyInto(f); + return f; + } + + /** + * Return all the user flags in this Flags object. Returns + * an array of size zero if no flags are set. + * + * @return array of Strings, each String represents a flag. + */ + public String[] getUserFlags() { + Vector v = new Vector<>(); + if (user_flags != null) { + Enumeration e = user_flags.elements(); + + while (e.hasMoreElements()) + v.addElement(e.nextElement()); + } + + String[] f = new String[v.size()]; + v.copyInto(f); + return f; + } + + /** + * Clear all of the system flags. + * + * @since JavaMail 1.6 + */ + public void clearSystemFlags() { + system_flags = 0; + } + + /** + * Clear all of the user flags. + * + * @since JavaMail 1.6 + */ + public void clearUserFlags() { + user_flags = null; + } + + /** + * Returns a clone of this Flags object. + */ + @SuppressWarnings("unchecked") + @Override + public Object clone() { + Flags f = null; + try { + f = (Flags) super.clone(); + } catch (CloneNotSupportedException cex) { + // ignore, can't happen + } + if (this.user_flags != null) + f.user_flags = (Hashtable) this.user_flags.clone(); + return f; + } + + /** + * Return a string representation of this Flags object. + * Note that the exact format of the string is subject to change. + */ + public String toString() { + StringBuilder sb = new StringBuilder(); + + if ((system_flags & ANSWERED_BIT) != 0) + sb.append("\\Answered "); + if ((system_flags & DELETED_BIT) != 0) + sb.append("\\Deleted "); + if ((system_flags & DRAFT_BIT) != 0) + sb.append("\\Draft "); + if ((system_flags & FLAGGED_BIT) != 0) + sb.append("\\Flagged "); + if ((system_flags & RECENT_BIT) != 0) + sb.append("\\Recent "); + if ((system_flags & SEEN_BIT) != 0) + sb.append("\\Seen "); + if ((system_flags & USER_BIT) != 0) + sb.append("\\* "); + + boolean first = true; + if (user_flags != null) { + Enumeration e = user_flags.elements(); + + while (e.hasMoreElements()) { + if (first) + first = false; + else + sb.append(' '); + sb.append(e.nextElement()); + } + } + + if (first && sb.length() > 0) + sb.setLength(sb.length() - 1); // smash trailing space + + return sb.toString(); + } + + /** + * This inner class represents an individual system flag. A set + * of standard system flag objects are predefined here. + */ + public static final class Flag { + /** + * This message has been answered. This flag is set by clients + * to indicate that this message has been answered to. + */ + public static final Flag ANSWERED = new Flag(ANSWERED_BIT); + + /** + * This message is marked deleted. Clients set this flag to + * mark a message as deleted. The expunge operation on a folder + * removes all messages in that folder that are marked for deletion. + */ + public static final Flag DELETED = new Flag(DELETED_BIT); + + /** + * This message is a draft. This flag is set by clients + * to indicate that the message is a draft message. + */ + public static final Flag DRAFT = new Flag(DRAFT_BIT); + + /** + * This message is flagged. No semantic is defined for this flag. + * Clients alter this flag. + */ + public static final Flag FLAGGED = new Flag(FLAGGED_BIT); + + /** + * This message is recent. Folder implementations set this flag + * to indicate that this message is new to this folder, that is, + * it has arrived since the last time this folder was opened.

+ *

+ * Clients cannot alter this flag. + */ + public static final Flag RECENT = new Flag(RECENT_BIT); + + /** + * This message is seen. This flag is implicitly set by the + * implementation when this Message's content is returned + * to the client in some form. The getInputStream + * and getContent methods on Message cause this + * flag to be set.

+ *

+ * Clients can alter this flag. + */ + public static final Flag SEEN = new Flag(SEEN_BIT); + + /** + * A special flag that indicates that this folder supports + * user defined flags.

+ *

+ * The implementation sets this flag. Clients cannot alter + * this flag but can use it to determine if a folder supports + * user defined flags by using + * folder.getPermanentFlags().contains(Flags.Flag.USER). + */ + public static final Flag USER = new Flag(USER_BIT); + + // flags are stored as bits for efficiency + private int bit; + + private Flag(int bit) { + this.bit = bit; + } + } + + /* + public static void main(String argv[]) throws Exception { + // a new flags object + Flags f1 = new Flags(); + f1.add(Flags.Flag.DELETED); + f1.add(Flags.Flag.SEEN); + f1.add(Flags.Flag.RECENT); + f1.add(Flags.Flag.ANSWERED); + + // check copy constructor with only system flags + Flags fc = new Flags(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone with only system flags + fc = (Flags)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // add a user flag and make sure it still works right + f1.add("MyFlag"); + + // shouldn't be equal here + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check clone + fc = (Flags)f1.clone(); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // make sure user flag hash tables are separate + fc.add("AnotherFlag"); + if (!f1.equals(fc) && !fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // check copy constructor + fc = new Flags(f1); + if (f1.equals(fc) && fc.equals(f1)) + System.out.println("success"); + else + System.out.println("fail"); + + // another new flags object + Flags f2 = new Flags(Flags.Flag.ANSWERED); + f2.add("MyFlag"); + + if (f1.contains(Flags.Flag.DELETED)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Flags.Flag.SEEN)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains(Flags.Flag.RECENT)) + System.out.println("success"); + else + System.out.println("fail"); + + if (f1.contains("MyFlag")) + System.out.println("success"); + else + System.out.println("fail"); + + if (f2.contains(Flags.Flag.ANSWERED)) + System.out.println("success"); + else + System.out.println("fail"); + + + System.out.println("----------------"); + + String[] s = f1.getUserFlags(); + for (int i = 0; i < s.length; i++) + System.out.println(s[i]); + System.out.println("----------------"); + s = f2.getUserFlags(); + for (int i = 0; i < s.length; i++) + System.out.println(s[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"); + + Flags f3 = new Flags(); + f3.add(Flags.Flag.DELETED); + f3.add(Flags.Flag.SEEN); + f3.add(Flags.Flag.RECENT); + f3.add(Flags.Flag.ANSWERED); + f3.add("ANOTHERFLAG"); + f3.add("MYFLAG"); + + f1.add("AnotherFlag"); + + 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/net-mail/src/main/java/jakarta/mail/Folder.java b/net-mail/src/main/java/jakarta/mail/Folder.java new file mode 100644 index 0000000..64a5d72 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Folder.java @@ -0,0 +1,1645 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import jakarta.mail.event.ConnectionEvent; +import jakarta.mail.event.ConnectionListener; +import jakarta.mail.event.FolderEvent; +import jakarta.mail.event.FolderListener; +import jakarta.mail.event.MailEvent; +import jakarta.mail.event.MessageChangedEvent; +import jakarta.mail.event.MessageChangedListener; +import jakarta.mail.event.MessageCountEvent; +import jakarta.mail.event.MessageCountListener; +import jakarta.mail.search.SearchTerm; +import java.util.ArrayList; +import java.util.EventListener; +import java.util.List; +import java.util.Vector; +import java.util.concurrent.Executor; + +/** + * Folder is an abstract class that represents a folder for mail + * messages. Subclasses implement protocol specific Folders.

+ *

+ * Folders can contain Messages, other Folders or both, thus providing + * a tree-like hierarchy rooted at the Store's default folder. (Note + * that some Folder implementations may not allow both Messages and + * other Folders in the same Folder).

+ *

+ * The interpretation of folder names is implementation dependent. + * The different levels of hierarchy in a folder's full name + * are separated from each other by the hierarchy delimiter + * character.

+ *

+ * The case-insensitive full folder name (that is, the full name + * relative to the default folder for a Store) INBOX + * is reserved to mean the "primary folder for this user on this + * server". Not all Stores will provide an INBOX folder, and not + * all users will have an INBOX folder at all times. The name + * INBOX is reserved to refer to this folder, + * when it exists, in Stores that provide it.

+ *

+ * A Folder object obtained from a Store need not actually exist + * in the backend store. The exists method tests whether + * the folder exists or not. The create method + * creates a Folder.

+ *

+ * A Folder is initially in the closed state. Certain methods are valid + * in this state; the documentation for those methods note this. A + * Folder is opened by calling its 'open' method. All Folder methods, + * except open, delete and + * renameTo, are valid in this state.

+ *

+ * The only way to get a Folder is by invoking the + * getFolder method on Store, Folder, or Session, or by invoking + * the list or listSubscribed methods + * on Folder. Folder objects returned by the above methods are not + * cached by the Store. Thus, invoking the getFolder method + * with the same folder name multiple times will return distinct Folder + * objects. Likewise for the list and listSubscribed + * methods.

+ *

+ * The Message objects within the Folder are cached by the Folder. + * Thus, invoking getMessage(msgno) on the same message number + * multiple times will return the same Message object, until an + * expunge is done on this Folder.

+ *

+ * Message objects from a Folder are only valid while a Folder is open + * and should not be accessed after the Folder is closed, even if the + * Folder is subsequently reopened. Instead, new Message objects must + * be fetched from the Folder after the Folder is reopened.

+ *

+ * Note that a Message's message number can change within a + * session if the containing Folder is expunged using the expunge + * method. Clients that use message numbers as references to messages + * should be aware of this and should be prepared to deal with this + * situation (probably by flushing out existing message number references + * and reloading them). Because of this complexity, it is better for + * clients to use Message objects as references to messages, rather than + * message numbers. Expunged Message objects still have to be + * pruned, but other Message objects in that folder are not affected by the + * expunge. + * + * @author John Mani + * @author Bill Shannon + */ + +public abstract class Folder implements AutoCloseable { + + /** + * This folder can contain messages + */ + public final static int HOLDS_MESSAGES = 0x01; + /** + * This folder can contain other folders + */ + public final static int HOLDS_FOLDERS = 0x02; + /** + * The Folder is read only. The state and contents of this + * folder cannot be modified. + */ + public static final int READ_ONLY = 1; + /** + * The state and contents of this folder can be modified. + */ + public static final int READ_WRITE = 2; + /* + * The queue of events to be delivered. + */ + private final EventQueue q; + /** + * The parent store. + */ + protected Store store; + /** + * The open mode of this folder. The open mode is + * Folder.READ_ONLY, Folder.READ_WRITE, + * or -1 if not known. + * + * @since JavaMail 1.1 + */ + protected int mode = -1; + // Vector of connection listeners. + private volatile Vector connectionListeners = null; + // Vector of folder listeners + private volatile Vector folderListeners = null; + // Vector of MessageCount listeners + private volatile Vector messageCountListeners = null; + // Vector of MessageChanged listeners. + private volatile Vector messageChangedListeners + = null; + + /** + * Constructor that takes a Store object. + * + * @param store the Store that holds this folder + */ + protected Folder(Store store) { + this.store = store; + + // create or choose the appropriate event queue + Session session = store.getSession(); + String scope = + session.getProperties().getProperty("mail.event.scope", "folder"); + Executor executor = + (Executor) session.getProperties().get("mail.event.executor"); + if (scope.equalsIgnoreCase("application")) + q = EventQueue.getApplicationEventQueue(executor); + else if (scope.equalsIgnoreCase("session")) + q = session.getEventQueue(); + else if (scope.equalsIgnoreCase("store")) + q = store.getEventQueue(); + else // if (scope.equalsIgnoreCase("folder")) + q = new EventQueue(executor); + } + + /** + * Returns the name of this Folder.

+ *

+ * This method can be invoked on a closed Folder. + * + * @return name of the Folder + */ + public abstract String getName(); + + /** + * Returns the full name of this Folder. If the folder resides under + * the root hierarchy of this Store, the returned name is relative + * to the root. Otherwise an absolute name, starting with the + * hierarchy delimiter, is returned.

+ *

+ * This method can be invoked on a closed Folder. + * + * @return full name of the Folder + */ + public abstract String getFullName(); + + /** + * Return a URLName representing this folder. The returned URLName + * does not include the password used to access the store. + * + * @return the URLName representing this folder + * @throws MessagingException for failures + * @see URLName + * @since JavaMail 1.1 + */ + public URLName getURLName() throws MessagingException { + URLName storeURL = getStore().getURLName(); + String fullname = getFullName(); + StringBuilder encodedName = new StringBuilder(); + + if (fullname != null) { + /* + // We need to encode each of the folder's names. + char separator = getSeparator(); + StringTokenizer tok = new StringTokenizer( + fullname, new Character(separator).toString(), true); + + while (tok.hasMoreTokens()) { + String s = tok.nextToken(); + if (s.charAt(0) == separator) + encodedName.append(separator); + else + // XXX - should encode, but since there's no decoder... + //encodedName.append(java.net.URLEncoder.encode(s)); + encodedName.append(s); + } + */ + // append the whole thing, until we can encode + encodedName.append(fullname); + } + + /* + * Sure would be convenient if URLName had a + * constructor that took a base URLName. + */ + return new URLName(storeURL.getProtocol(), storeURL.getHost(), + storeURL.getPort(), encodedName.toString(), + storeURL.getUsername(), + null /* no password */); + } + + /** + * Returns the Store that owns this Folder object. + * This method can be invoked on a closed Folder. + * + * @return the Store + */ + public Store getStore() { + return store; + } + + /** + * Returns the parent folder of this folder. + * This method can be invoked on a closed Folder. If this folder + * is the top of a folder hierarchy, this method returns null.

+ *

+ * Note that since Folder objects are not cached, invoking this method + * returns a new distinct Folder object. + * + * @return Parent folder + * @throws MessagingException for failures + */ + public abstract Folder getParent() throws MessagingException; + + /** + * Tests if this folder physically exists on the Store. + * This method can be invoked on a closed Folder. + * + * @return true if the folder exists, otherwise false + * @throws MessagingException typically if the connection + * to the server is lost. + * @see #create + */ + public abstract boolean exists() throws MessagingException; + + /** + * Returns a list of Folders belonging to this Folder's namespace + * that match the specified pattern. Patterns may contain the wildcard + * characters "%", which matches any character except hierarchy + * delimiters, and "*", which matches any character.

+ *

+ * As an example, given the folder hierarchy:

+     *    Personal/
+     *       Finance/
+     *          Stocks
+     *          Bonus
+     *          StockOptions
+     *       Jokes
+     * 
+ * list("*") on "Personal" will return the whole + * hierarchy.
+ * list("%") on "Personal" will return "Finance" and + * "Jokes".
+ * list("Jokes") on "Personal" will return "Jokes".
+ * list("Stock*") on "Finance" will return "Stocks" + * and "StockOptions".

+ *

+ * Folder objects are not cached by the Store, so invoking this + * method on the same pattern multiple times will return that many + * distinct Folder objects.

+ *

+ * This method can be invoked on a closed Folder. + * + * @param pattern the match pattern + * @return array of matching Folder objects. An empty + * array is returned if no matching Folders exist. + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws MessagingException for other failures + * @see #listSubscribed + */ + public abstract Folder[] list(String pattern) throws MessagingException; + + /** + * Returns a list of subscribed Folders belonging to this Folder's + * namespace that match the specified pattern. If the folder does + * not support subscription, this method should resolve to + * list. + * (The default implementation provided here, does just this). + * The pattern can contain wildcards as for list.

+ *

+ * Note that, at a given level of the folder hierarchy, a particular + * folder may not be subscribed, but folders underneath that folder + * in the folder hierarchy may be subscribed. In order to allow + * walking the folder hierarchy, such unsubscribed folders may be + * returned, indicating that a folder lower in the hierarchy is + * subscribed. The isSubscribed method on a folder will + * tell whether any particular folder is actually subscribed.

+ *

+ * Folder objects are not cached by the Store, so invoking this + * method on the same pattern multiple times will return that many + * distinct Folder objects.

+ *

+ * This method can be invoked on a closed Folder. + * + * @param pattern the match pattern + * @return array of matching subscribed Folder objects. An + * empty array is returned if no matching + * subscribed folders exist. + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws MessagingException for other failures + * @see #list + */ + public Folder[] listSubscribed(String pattern) throws MessagingException { + return list(pattern); + } + + /** + * Convenience method that returns the list of folders under this + * Folder. This method just calls the list(String pattern) + * method with "%" as the match pattern. This method can + * be invoked on a closed Folder. + * + * @return array of Folder objects under this Folder. An + * empty array is returned if no subfolders exist. + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws MessagingException for other failures + * @see #list + */ + + public Folder[] list() throws MessagingException { + return list("%"); + } + + /** + * Convenience method that returns the list of subscribed folders + * under this Folder. This method just calls the + * listSubscribed(String pattern) method with "%" + * as the match pattern. This method can be invoked on a closed Folder. + * + * @return array of subscribed Folder objects under this + * Folder. An empty array is returned if no subscribed + * subfolders exist. + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws MessagingException for other failures + * @see #listSubscribed + */ + public Folder[] listSubscribed() throws MessagingException { + return listSubscribed("%"); + } + + /** + * Return the delimiter character that separates this Folder's pathname + * from the names of immediate subfolders. This method can be invoked + * on a closed Folder. + * + * @return Hierarchy separator character + * @throws FolderNotFoundException if the implementation + * requires the folder to exist, but it does not + */ + public abstract char getSeparator() throws MessagingException; + + /** + * Returns the type of this Folder, that is, whether this folder can hold + * messages or subfolders or both. The returned value is an integer + * bitfield with the appropriate bits set. This method can be invoked + * on a closed folder. + * + * @return integer with appropriate bits set + * @throws FolderNotFoundException if this folder does + * not exist. + * @see #HOLDS_MESSAGES + * @see #HOLDS_FOLDERS + */ + public abstract int getType() throws MessagingException; + + /** + * Create this folder on the Store. When this folder is created, any + * folders in its path that do not exist are also created.

+ *

+ * If the creation is successful, a CREATED FolderEvent is delivered + * to any FolderListeners registered on this Folder and this Store. + * + * @param type The type of this folder. + * @return true if the creation succeeds, else false. + * @throws MessagingException for failures + * @see #HOLDS_MESSAGES + * @see FolderEvent + * @see #HOLDS_FOLDERS + */ + public abstract boolean create(int type) throws MessagingException; + + /** + * Returns true if this Folder is subscribed.

+ *

+ * This method can be invoked on a closed Folder.

+ *

+ * The default implementation provided here just returns true. + * + * @return true if this Folder is subscribed + */ + public boolean isSubscribed() { + return true; + } + + /** + * Subscribe or unsubscribe this Folder. Not all Stores support + * subscription.

+ *

+ * This method can be invoked on a closed Folder.

+ *

+ * The implementation provided here just throws the + * MethodNotSupportedException. + * + * @param subscribe true to subscribe, false to unsubscribe + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws MethodNotSupportedException if this store + * does not support subscription + * @throws MessagingException for other failures + */ + public void setSubscribed(boolean subscribe) + throws MessagingException { + throw new MethodNotSupportedException(); + } + + /** + * Returns true if this Folder has new messages since the last time + * this indication was reset. When this indication is set or reset + * depends on the Folder implementation (and in the case of IMAP, + * depends on the server). This method can be used to implement + * a lightweight "check for new mail" operation on a Folder without + * opening it. (For example, a thread that monitors a mailbox and + * flags when it has new mail.) This method should indicate whether + * any messages in the Folder have the RECENT flag set.

+ *

+ * Note that this is not an incremental check for new mail, i.e., + * it cannot be used to determine whether any new messages have + * arrived since the last time this method was invoked. To + * implement incremental checks, the Folder needs to be opened.

+ *

+ * This method can be invoked on a closed Folder that can contain + * Messages. + * + * @return true if the Store has new Messages + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws MessagingException for other failures + */ + public abstract boolean hasNewMessages() throws MessagingException; + + /** + * Return the Folder object corresponding to the given name. Note that + * this folder does not physically have to exist in the Store. The + * exists() method on a Folder indicates whether it really + * exists on the Store.

+ *

+ * In some Stores, name can be an absolute path if it starts with the + * hierarchy delimiter. Otherwise, it is interpreted relative to + * this Folder.

+ *

+ * Folder objects are not cached by the Store, so invoking this + * method on the same name multiple times will return that many + * distinct Folder objects.

+ *

+ * This method can be invoked on a closed Folder. + * + * @param name name of the Folder + * @return Folder object + * @throws MessagingException for failures + */ + public abstract Folder getFolder(String name) + throws MessagingException; + + /** + * Delete this Folder. This method will succeed only on a closed + * Folder.

+ *

+ * The recurse flag controls whether the deletion affects + * subfolders or not. If true, all subfolders are deleted, then this + * folder itself is deleted. If false, the behaviour is dependent on + * the folder type and is elaborated below: + * + *

    + *
  • + * The folder can contain only messages: (type == HOLDS_MESSAGES). + *
    + * All messages within the folder are removed. The folder + * itself is then removed. An appropriate FolderEvent is generated by + * the Store and this folder. + * + *
  • + * The folder can contain only subfolders: (type == HOLDS_FOLDERS). + *
    + * If this folder is empty (does not contain any + * subfolders at all), it is removed. An appropriate FolderEvent is + * generated by the Store and this folder.
    + * If this folder contains any subfolders, the delete fails + * and returns false. + * + *
  • + * The folder can contain subfolders as well as messages:
    + * If the folder is empty (no messages or subfolders), it + * is removed. If the folder contains no subfolders, but only messages, + * then all messages are removed. The folder itself is then removed. + * In both the above cases, an appropriate FolderEvent is + * generated by the Store and this folder.

    + *

    + * If the folder contains subfolders there are 3 possible + * choices an implementation is free to do: + * + *

      + *
    1. The operation fails, irrespective of whether this folder + * contains messages or not. Some implementations might elect to go + * with this simple approach. The delete() method returns false. + * + *
    2. Any messages within the folder are removed. Subfolders + * are not removed. The folder itself is not removed or affected + * in any manner. The delete() method returns true. And the + * exists() method on this folder will return true indicating that + * this folder still exists.
      + * An appropriate FolderEvent is generated by the Store and this folder. + * + *
    3. Any messages within the folder are removed. Subfolders are + * not removed. The folder itself changes its type from + * HOLDS_FOLDERS | HOLDS_MESSAGES to HOLDS_FOLDERS. Thus new + * messages cannot be added to this folder, but new subfolders can + * be created underneath. The delete() method returns true indicating + * success. The exists() method on this folder will return true + * indicating that this folder still exists.
      + * An appropriate FolderEvent is generated by the Store and this folder. + *
    + *
+ * + * @param recurse also delete subfolders? + * @return true if the Folder is deleted successfully + * @throws FolderNotFoundException if this folder does + * not exist + * @throws IllegalStateException if this folder is not in + * the closed state. + * @throws MessagingException for other failures + * @see FolderEvent + */ + public abstract boolean delete(boolean recurse) + throws MessagingException; + + /** + * Rename this Folder. This method will succeed only on a closed + * Folder.

+ *

+ * If the rename is successful, a RENAMED FolderEvent is delivered + * to FolderListeners registered on this folder and its containing + * Store. + * + * @param f a folder representing the new name for this Folder + * @return true if the Folder is renamed successfully + * @throws FolderNotFoundException if this folder does + * not exist + * @throws IllegalStateException if this folder is not in + * the closed state. + * @throws MessagingException for other failures + * @see FolderEvent + */ + public abstract boolean renameTo(Folder f) throws MessagingException; + + /** + * Open this Folder. This method is valid only on Folders that + * can contain Messages and that are closed.

+ *

+ * If this folder is opened successfully, an OPENED ConnectionEvent + * is delivered to any ConnectionListeners registered on this + * Folder.

+ *

+ * The effect of opening multiple connections to the same folder + * on a specifc Store is implementation dependent. Some implementations + * allow multiple readers, but only one writer. Others allow + * multiple writers as well as readers. + * + * @param mode open the Folder READ_ONLY or READ_WRITE + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws IllegalStateException if this folder is not in + * the closed state. + * @throws MessagingException for other failures + * @see #READ_ONLY + * @see #READ_WRITE + * @see #getType() + * @see ConnectionEvent + */ + public abstract void open(int mode) throws MessagingException; + + /** + * Close this Folder. This method is valid only on open Folders.

+ *

+ * A CLOSED ConnectionEvent is delivered to any ConnectionListeners + * registered on this Folder. Note that the folder is closed even + * if this method terminates abnormally by throwing a + * MessagingException. + * + * @param expunge expunges all deleted messages if this flag is true + * @throws IllegalStateException if this folder is not opened + * @throws MessagingException for other failures + * @see ConnectionEvent + */ + public abstract void close(boolean expunge) throws MessagingException; + + /** + * Close this Folder and expunge deleted messages.

+ *

+ * A CLOSED ConnectionEvent is delivered to any ConnectionListeners + * registered on this Folder. Note that the folder is closed even + * if this method terminates abnormally by throwing a + * MessagingException.

+ *

+ * This method supports the {@link AutoCloseable AutoCloseable} + * interface.

+ *

+ * This implementation calls close(true). + * + * @throws IllegalStateException if this folder is not opened + * @throws MessagingException for other failures + * @see ConnectionEvent + * @since JavaMail 1.6 + */ + @Override + public void close() throws MessagingException { + close(true); + } + + /** + * Indicates whether this Folder is in the 'open' state. + * + * @return true if this Folder is in the 'open' state. + */ + public abstract boolean isOpen(); + + /** + * Return the open mode of this folder. Returns + * Folder.READ_ONLY, Folder.READ_WRITE, + * or -1 if the open mode is not known (usually only because an older + * Folder provider has not been updated to use this new + * method). + * + * @return the open mode of this folder + * @throws IllegalStateException if this folder is not opened + * @since JavaMail 1.1 + */ + public synchronized int getMode() { + if (!isOpen()) + throw new IllegalStateException("Folder not open"); + return mode; + } + + /** + * Get the permanent flags supported by this Folder. Returns a Flags + * object that contains all the flags supported.

+ *

+ * The special flag Flags.Flag.USER indicates that this Folder + * supports arbitrary user-defined flags.

+ *

+ * The supported permanent flags for a folder may not be available + * until the folder is opened. + * + * @return permanent flags, or null if not known + */ + public abstract Flags getPermanentFlags(); + + /** + * Get total number of messages in this Folder.

+ *

+ * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the total message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1.

+ *

+ * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case. + * + * @return total number of messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws MessagingException for other failures + */ + public abstract int getMessageCount() throws MessagingException; + + /** + * Get the number of new messages in this Folder.

+ *

+ * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the new message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1.

+ *

+ * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case.

+ *

+ * This implementation returns -1 if this folder is closed. Else + * this implementation gets each Message in the folder using + * getMessage(int) and checks whether its + * RECENT flag is set. The total number of messages + * that have this flag set is returned. + * + * @return number of new messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws MessagingException for other failures + */ + public synchronized int getNewMessageCount() + throws MessagingException { + if (!isOpen()) + return -1; + + int newmsgs = 0; + int total = getMessageCount(); + for (int i = 1; i <= total; i++) { + try { + if (getMessage(i).isSet(Flags.Flag.RECENT)) + newmsgs++; + } catch (MessageRemovedException me) { + // This is an expunged message, ignore it. + continue; + } + } + return newmsgs; + } + + /** + * Get the total number of unread messages in this Folder.

+ *

+ * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the unread message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1.

+ *

+ * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case.

+ *

+ * This implementation returns -1 if this folder is closed. Else + * this implementation gets each Message in the folder using + * getMessage(int) and checks whether its + * SEEN flag is set. The total number of messages + * that do not have this flag set is returned. + * + * @return total number of unread messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws MessagingException for other failures + */ + public synchronized int getUnreadMessageCount() + throws MessagingException { + if (!isOpen()) + return -1; + + int unread = 0; + int total = getMessageCount(); + for (int i = 1; i <= total; i++) { + try { + if (!getMessage(i).isSet(Flags.Flag.SEEN)) + unread++; + } catch (MessageRemovedException me) { + // This is an expunged message, ignore it. + continue; + } + } + return unread; + } + + /** + * Get the number of deleted messages in this Folder.

+ *

+ * This method can be invoked on a closed folder. However, note + * that for some folder implementations, getting the deleted message + * count can be an expensive operation involving actually opening + * the folder. In such cases, a provider can choose not to support + * this functionality in the closed state, in which case this method + * must return -1.

+ *

+ * Clients invoking this method on a closed folder must be aware + * that this is a potentially expensive operation. Clients must + * also be prepared to handle a return value of -1 in this case.

+ *

+ * This implementation returns -1 if this folder is closed. Else + * this implementation gets each Message in the folder using + * getMessage(int) and checks whether its + * DELETED flag is set. The total number of messages + * that have this flag set is returned. + * + * @return number of deleted messages. -1 may be returned + * by certain implementations if this method is + * invoked on a closed folder. + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws MessagingException for other failures + * @since JavaMail 1.3 + */ + public synchronized int getDeletedMessageCount() throws MessagingException { + if (!isOpen()) + return -1; + + int deleted = 0; + int total = getMessageCount(); + for (int i = 1; i <= total; i++) { + try { + if (getMessage(i).isSet(Flags.Flag.DELETED)) + deleted++; + } catch (MessageRemovedException me) { + // This is an expunged message, ignore it. + continue; + } + } + return deleted; + } + + /** + * Get the Message object corresponding to the given message + * number. A Message object's message number is the relative + * position of this Message in its Folder. Messages are numbered + * starting at 1 through the total number of message in the folder. + * Note that the message number for a particular Message can change + * during a session if other messages in the Folder are deleted and + * the Folder is expunged.

+ *

+ * Message objects are light-weight references to the actual message + * that get filled up on demand. Hence Folder implementations are + * expected to provide light-weight Message objects.

+ *

+ * Unlike Folder objects, repeated calls to getMessage with the + * same message number will return the same Message object, as + * long as no messages in this folder have been expunged.

+ *

+ * Since message numbers can change within a session if the folder + * is expunged , clients are advised not to use message numbers as + * references to messages. Use Message objects instead. + * + * @param msgnum the message number + * @return the Message object + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws IllegalStateException if this folder is not opened + * @throws IndexOutOfBoundsException if the message number + * is out of range. + * @throws MessagingException for other failures + * @see #getMessageCount + * @see #fetch + */ + public abstract Message getMessage(int msgnum) + throws MessagingException; + + /** + * Get the Message objects for message numbers ranging from start + * through end, both start and end inclusive. Note that message + * numbers start at 1, not 0.

+ *

+ * Message objects are light-weight references to the actual message + * that get filled up on demand. Hence Folder implementations are + * expected to provide light-weight Message objects.

+ *

+ * This implementation uses getMessage(index) to obtain the required + * Message objects. Note that the returned array must contain + * (end-start+1) Message objects. + * + * @param start the number of the first message + * @param end the number of the last message + * @return the Message objects + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws IllegalStateException if this folder is not opened. + * @throws IndexOutOfBoundsException if the start or end + * message numbers are out of range. + * @throws MessagingException for other failures + * @see #fetch + */ + public synchronized Message[] getMessages(int start, int end) + throws MessagingException { + Message[] msgs = new Message[end - start + 1]; + for (int i = start; i <= end; i++) + msgs[i - start] = getMessage(i); + return msgs; + } + + /** + * Get the Message objects for message numbers specified in + * the array.

+ *

+ * Message objects are light-weight references to the actual message + * that get filled up on demand. Hence Folder implementations are + * expected to provide light-weight Message objects.

+ *

+ * This implementation uses getMessage(index) to obtain the required + * Message objects. Note that the returned array must contain + * msgnums.length Message objects + * + * @param msgnums the array of message numbers + * @return the array of Message objects. + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws IllegalStateException if this folder is not opened. + * @throws IndexOutOfBoundsException if any message number + * in the given array is out of range. + * @throws MessagingException for other failures + * @see #fetch + */ + public synchronized Message[] getMessages(int[] msgnums) + throws MessagingException { + int len = msgnums.length; + Message[] msgs = new Message[len]; + for (int i = 0; i < len; i++) + msgs[i] = getMessage(msgnums[i]); + return msgs; + } + + /** + * Get all Message objects from this Folder. Returns an empty array + * if the folder is empty. + *

+ * Clients can use Message objects (instead of sequence numbers) + * as references to the messages within a folder; this method supplies + * the Message objects to the client. Folder implementations are + * expected to provide light-weight Message objects, which get + * filled on demand.

+ *

+ * This implementation invokes getMessageCount() to get + * the current message count and then uses getMessage() + * to get Message objects from 1 till the message count. + * + * @return array of Message objects, empty array if folder + * is empty. + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws IllegalStateException if this folder is not opened. + * @throws MessagingException for other failures + * @see #fetch + */ + public synchronized Message[] getMessages() throws MessagingException { + if (!isOpen()) // otherwise getMessageCount might return -1 + throw new IllegalStateException("Folder not open"); + int total = getMessageCount(); + Message[] msgs = new Message[total]; + for (int i = 1; i <= total; i++) + msgs[i - 1] = getMessage(i); + return msgs; + } + + /** + * Append given Messages to this folder. This method can be + * invoked on a closed Folder. An appropriate MessageCountEvent + * is delivered to any MessageCountListener registered on this + * folder when the messages arrive in the folder.

+ *

+ * Folder implementations must not abort this operation if a + * Message in the given message array turns out to be an + * expunged Message. + * + * @param msgs array of Messages to be appended + * @throws MessagingException if the append failed. + * @throws FolderNotFoundException if this folder does + * not exist. + */ + public abstract void appendMessages(Message[] msgs) + throws MessagingException; + + /** + * Prefetch the items specified in the FetchProfile for the + * given Messages.

+ *

+ * Clients use this method to indicate that the specified items are + * needed en-masse for the given message range. Implementations are + * expected to retrieve these items for the given message range in + * a efficient manner. Note that this method is just a hint to the + * implementation to prefetch the desired items.

+ *

+ * An example is a client filling its header-view window with + * the Subject, From and X-mailer headers for all messages in the + * folder. + *

+     *
+     *  Message[] msgs = folder.getMessages();
+     *
+     *  FetchProfile fp = new FetchProfile();
+     *  fp.add(FetchProfile.Item.ENVELOPE);
+     *  fp.add("X-mailer");
+     *  folder.fetch(msgs, fp);
+     *
+     *  for (int i = 0; i < folder.getMessageCount(); i++) {
+     *      display(msg[i].getFrom());
+     *      display(msg[i].getSubject());
+     *      display(msg[i].getHeader("X-mailer"));
+     *  }
+     *
+     * 

+ *

+ * The implementation provided here just returns without + * doing anything useful. Providers wanting to provide a real + * implementation for this method should override this method. + * + * @param msgs fetch items for these messages + * @param fp the FetchProfile + * @throws IllegalStateException if this folder is not opened + * @throws MessagingException for other failures + */ + public void fetch(Message[] msgs, FetchProfile fp) + throws MessagingException { + return; + } + + /** + * Set the specified flags on the messages specified in the array. + * This will result in appropriate MessageChangedEvents being + * delivered to any MessageChangedListener registered on this + * Message's containing folder.

+ *

+ * Note that the specified Message objects must + * belong to this folder. Certain Folder implementations can + * optimize the operation of setting Flags for a group of messages, + * so clients might want to use this method, rather than invoking + * Message.setFlags for each Message.

+ *

+ * This implementation degenerates to invoking setFlags() + * on each Message object. Specific Folder implementations that can + * optimize this case should do so. + * Also, an implementation must not abort the operation if a Message + * in the array turns out to be an expunged Message. + * + * @param msgs the array of message objects + * @param flag Flags object containing the flags to be set + * @param value set the flags to this boolean value + * @throws IllegalStateException if this folder is not opened + * or if it has been opened READ_ONLY. + * @throws MessagingException for other failures + * @see Message#setFlags + * @see MessageChangedEvent + */ + public synchronized void setFlags(Message[] msgs, + Flags flag, boolean value) throws MessagingException { + for (int i = 0; i < msgs.length; i++) { + try { + msgs[i].setFlags(flag, value); + } catch (MessageRemovedException me) { + // This message is expunged, skip + } + } + } + + /** + * Set the specified flags on the messages numbered from start + * through end, both start and end inclusive. Note that message + * numbers start at 1, not 0. + * This will result in appropriate MessageChangedEvents being + * delivered to any MessageChangedListener registered on this + * Message's containing folder.

+ *

+ * Certain Folder implementations can + * optimize the operation of setting Flags for a group of messages, + * so clients might want to use this method, rather than invoking + * Message.setFlags for each Message.

+ *

+ * The default implementation uses getMessage(int) to + * get each Message object and then invokes + * setFlags on that object to set the flags. + * Specific Folder implementations that can optimize this case should do so. + * Also, an implementation must not abort the operation if a message + * number refers to an expunged message. + * + * @param start the number of the first message + * @param end the number of the last message + * @param flag Flags object containing the flags to be set + * @param value set the flags to this boolean value + * @throws IllegalStateException if this folder is not opened + * or if it has been opened READ_ONLY. + * @throws IndexOutOfBoundsException if the start or end + * message numbers are out of range. + * @throws MessagingException for other failures + * @see Message#setFlags + * @see MessageChangedEvent + */ + public synchronized void setFlags(int start, int end, + Flags flag, boolean value) throws MessagingException { + for (int i = start; i <= end; i++) { + try { + Message msg = getMessage(i); + msg.setFlags(flag, value); + } catch (MessageRemovedException me) { + // This message is expunged, skip + } + } + } + + /** + * Set the specified flags on the messages whose message numbers + * are in the array. + * This will result in appropriate MessageChangedEvents being + * delivered to any MessageChangedListener registered on this + * Message's containing folder.

+ *

+ * Certain Folder implementations can + * optimize the operation of setting Flags for a group of messages, + * so clients might want to use this method, rather than invoking + * Message.setFlags for each Message.

+ *

+ * The default implementation uses getMessage(int) to + * get each Message object and then invokes + * setFlags on that object to set the flags. + * Specific Folder implementations that can optimize this case should do so. + * Also, an implementation must not abort the operation if a message + * number refers to an expunged message. + * + * @param msgnums the array of message numbers + * @param flag Flags object containing the flags to be set + * @param value set the flags to this boolean value + * @throws IllegalStateException if this folder is not opened + * or if it has been opened READ_ONLY. + * @throws IndexOutOfBoundsException if any message number + * in the given array is out of range. + * @throws MessagingException for other failures + * @see Message#setFlags + * @see MessageChangedEvent + */ + public synchronized void setFlags(int[] msgnums, + Flags flag, boolean value) throws MessagingException { + for (int i = 0; i < msgnums.length; i++) { + try { + Message msg = getMessage(msgnums[i]); + msg.setFlags(flag, value); + } catch (MessageRemovedException me) { + // This message is expunged, skip + } + } + } + + /* + * The set of listeners are stored in Vectors appropriate to their + * type. We mark all listener Vectors as "volatile" because, while + * we initialize them inside this folder's synchronization lock, + * they are accessed (checked for null) in the "notify" methods, + * which can't be synchronized due to lock ordering constraints. + * Since the listener fields (the handles on the Vector objects) + * are only ever set, and are never cleared, we believe this is + * safe. The code that dispatches the notifications will either + * see the null and assume there are no listeners or will see the + * Vector and will process the listeners. There's an inherent race + * between adding a listener and notifying the listeners; the lack + * of synchronization during notification does not make the race + * condition significantly worse. If one thread is setting a + * listener at the "same" time an event is being dispatched, the + * dispatch code might not see the listener right away. The + * dispatch code doesn't have to worry about the Vector handle + * being set to null, and thus using an out-of-date set of + * listeners, because we never set the field to null. + */ + + /** + * Copy the specified Messages from this Folder into another + * Folder. This operation appends these Messages to the + * destination Folder. The destination Folder does not have to + * be opened. An appropriate MessageCountEvent + * is delivered to any MessageCountListener registered on the + * destination folder when the messages arrive in the folder.

+ *

+ * Note that the specified Message objects must + * belong to this folder. Folder implementations might be able + * to optimize this method by doing server-side copies.

+ *

+ * This implementation just invokes appendMessages() + * on the destination folder to append the given Messages. Specific + * folder implementations that support server-side copies should + * do so, if the destination folder's Store is the same as this + * folder's Store. + * Also, an implementation must not abort the operation if a + * Message in the array turns out to be an expunged Message. + * + * @param msgs the array of message objects + * @param folder the folder to copy the messages to + * @throws FolderNotFoundException if the destination + * folder does not exist. + * @throws IllegalStateException if this folder is not opened. + * @throws MessagingException for other failures + * @see #appendMessages + */ + public void copyMessages(Message[] msgs, Folder folder) + throws MessagingException { + if (!folder.exists()) + throw new FolderNotFoundException( + folder.getFullName() + " does not exist", + folder); + + folder.appendMessages(msgs); + } + + /** + * Expunge (permanently remove) messages marked DELETED. Returns an + * array containing the expunged message objects. The + * getMessageNumber method + * on each of these message objects returns that Message's original + * (that is, prior to the expunge) sequence number. A MessageCountEvent + * containing the expunged messages is delivered to any + * MessageCountListeners registered on the folder.

+ *

+ * Expunge causes the renumbering of Message objects subsequent to + * the expunged messages. Clients that use message numbers as + * references to messages should be aware of this and should be + * prepared to deal with the situation (probably by flushing out + * existing message number caches and reloading them). Because of + * this complexity, it is better for clients to use Message objects + * as references to messages, rather than message numbers. Any + * expunged Messages objects still have to be pruned, but other + * Messages in that folder are not affected by the expunge.

+ *

+ * After a message is expunged, only the isExpunged and + * getMessageNumber methods are still valid on the + * corresponding Message object; other methods may throw + * MessageRemovedException + * + * @return array of expunged Message objects + * @throws FolderNotFoundException if this folder does not + * exist + * @throws IllegalStateException if this folder is not opened. + * @throws MessagingException for other failures + * @see Message#isExpunged + * @see MessageCountEvent + */ + public abstract Message[] expunge() throws MessagingException; + + /** + * Search this Folder for messages matching the specified + * search criterion. Returns an array containing the matching + * messages . Returns an empty array if no matches were found.

+ *

+ * This implementation invokes + * search(term, getMessages()), to apply the search + * over all the messages in this folder. Providers that can implement + * server-side searching might want to override this method to provide + * a more efficient implementation. + * + * @param term the search criterion + * @return array of matching messages + * @throws jakarta.mail.search.SearchException if the search + * term is too complex for the implementation to handle. + * @throws FolderNotFoundException if this folder does + * not exist. + * @throws IllegalStateException if this folder is not opened. + * @throws MessagingException for other failures + * @see SearchTerm + */ + public Message[] search(SearchTerm term) throws MessagingException { + return search(term, getMessages()); + } + + /** + * Search the given array of messages for those that match the + * specified search criterion. Returns an array containing the + * matching messages. Returns an empty array if no matches were + * found.

+ *

+ * Note that the specified Message objects must + * belong to this folder.

+ *

+ * This implementation iterates through the given array of messages, + * and applies the search criterion on each message by calling + * its match() method with the given term. The + * messages that succeed in the match are returned. Providers + * that can implement server-side searching might want to override + * this method to provide a more efficient implementation. If the + * search term is too complex or contains user-defined terms that + * cannot be executed on the server, providers may elect to either + * throw a SearchException or degenerate to client-side searching by + * calling super.search() to invoke this implementation. + * + * @param term the search criterion + * @param msgs the messages to be searched + * @return array of matching messages + * @throws jakarta.mail.search.SearchException if the search + * term is too complex for the implementation to handle. + * @throws IllegalStateException if this folder is not opened + * @throws MessagingException for other failures + * @see SearchTerm + */ + public Message[] search(SearchTerm term, Message[] msgs) + throws MessagingException { + List matchedMsgs = new ArrayList<>(); + + // Run thru the given messages + for (Message msg : msgs) { + try { + if (msg.match(term)) // matched + matchedMsgs.add(msg); // add it + } catch (MessageRemovedException mrex) { + } + } + + return matchedMsgs.toArray(new Message[0]); + } + + /** + * Add a listener for Connection events on this Folder.

+ *

+ * The implementation provided here adds this listener + * to an internal list of ConnectionListeners. + * + * @param l the Listener for Connection events + * @see ConnectionEvent + */ + public synchronized void + addConnectionListener(ConnectionListener l) { + if (connectionListeners == null) + connectionListeners = new Vector<>(); + connectionListeners.addElement(l); + } + + /** + * Remove a Connection event listener.

+ *

+ * The implementation provided here removes this listener + * from the internal list of ConnectionListeners. + * + * @param l the listener + * @see #addConnectionListener + */ + public synchronized void + removeConnectionListener(ConnectionListener l) { + if (connectionListeners != null) + connectionListeners.removeElement(l); + } + + /** + * Notify all ConnectionListeners. Folder implementations are + * expected to use this method to broadcast connection events.

+ *

+ * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * ConnectionListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the ConnectionEvent type + * @see ConnectionEvent + */ + protected void notifyConnectionListeners(int type) { + if (connectionListeners != null) { + ConnectionEvent e = new ConnectionEvent(this, type); + queueEvent(e, connectionListeners); + } + + /* Fix for broken JDK1.1.x Garbage collector : + * The 'conservative' GC in JDK1.1.x occasionally fails to + * garbage-collect Threads which are in the wait state. + * This would result in thread (and consequently memory) leaks. + * + * We attempt to fix this by sending a 'terminator' event + * to the queue, after we've sent the CLOSED event. The + * terminator event causes the event-dispatching thread to + * self destruct. + */ + if (type == ConnectionEvent.CLOSED) + q.terminateQueue(); + } + + /** + * Add a listener for Folder events on this Folder.

+ *

+ * The implementation provided here adds this listener + * to an internal list of FolderListeners. + * + * @param l the Listener for Folder events + * @see FolderEvent + */ + public synchronized void addFolderListener(FolderListener l) { + if (folderListeners == null) + folderListeners = new Vector<>(); + folderListeners.addElement(l); + } + + /** + * Remove a Folder event listener.

+ *

+ * The implementation provided here removes this listener + * from the internal list of FolderListeners. + * + * @param l the listener + * @see #addFolderListener + */ + public synchronized void removeFolderListener(FolderListener l) { + if (folderListeners != null) + folderListeners.removeElement(l); + } + + /** + * Notify all FolderListeners registered on this Folder and + * this folder's Store. Folder implementations are expected + * to use this method to broadcast Folder events.

+ *

+ * The implementation provided here queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the + * FolderListeners registered on this folder. The implementation + * also invokes notifyFolderListeners on this folder's + * Store to notify any FolderListeners registered on the store. + * + * @param type type of FolderEvent + * @see #notifyFolderRenamedListeners + */ + protected void notifyFolderListeners(int type) { + if (folderListeners != null) { + FolderEvent e = new FolderEvent(this, this, type); + queueEvent(e, folderListeners); + } + store.notifyFolderListeners(type, this); + } + + /** + * Notify all FolderListeners registered on this Folder and + * this folder's Store about the renaming of this folder. + * Folder implementations are expected to use this method to + * broadcast Folder events indicating the renaming of folders.

+ *

+ * The implementation provided here queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the + * FolderListeners registered on this folder. The implementation + * also invokes notifyFolderRenamedListeners on this + * folder's Store to notify any FolderListeners registered on the store. + * + * @param folder Folder representing the new name. + * @see #notifyFolderListeners + * @since JavaMail 1.1 + */ + protected void notifyFolderRenamedListeners(Folder folder) { + if (folderListeners != null) { + FolderEvent e = new FolderEvent(this, this, folder, + FolderEvent.RENAMED); + queueEvent(e, folderListeners); + } + store.notifyFolderRenamedListeners(this, folder); + } + + /** + * Add a listener for MessageCount events on this Folder.

+ *

+ * The implementation provided here adds this listener + * to an internal list of MessageCountListeners. + * + * @param l the Listener for MessageCount events + * @see MessageCountEvent + */ + public synchronized void addMessageCountListener(MessageCountListener l) { + if (messageCountListeners == null) + messageCountListeners = new Vector<>(); + messageCountListeners.addElement(l); + } + + /** + * Remove a MessageCount listener.

+ *

+ * The implementation provided here removes this listener + * from the internal list of MessageCountListeners. + * + * @param l the listener + * @see #addMessageCountListener + */ + public synchronized void + removeMessageCountListener(MessageCountListener l) { + if (messageCountListeners != null) + messageCountListeners.removeElement(l); + } + + /** + * Notify all MessageCountListeners about the addition of messages + * into this folder. Folder implementations are expected to use this + * method to broadcast MessageCount events for indicating arrival of + * new messages.

+ *

+ * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * MessageCountListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param msgs the messages that were added + */ + protected void notifyMessageAddedListeners(Message[] msgs) { + if (messageCountListeners == null) + return; + + MessageCountEvent e = new MessageCountEvent( + this, + MessageCountEvent.ADDED, + false, + msgs); + + queueEvent(e, messageCountListeners); + } + + /** + * Notify all MessageCountListeners about the removal of messages + * from this Folder. Folder implementations are expected to use this + * method to broadcast MessageCount events indicating removal of + * messages.

+ *

+ * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * MessageCountListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param removed was the message removed by this client? + * @param msgs the messages that were removed + */ + protected void notifyMessageRemovedListeners(boolean removed, + Message[] msgs) { + if (messageCountListeners == null) + return; + + MessageCountEvent e = new MessageCountEvent( + this, + MessageCountEvent.REMOVED, + removed, + msgs); + queueEvent(e, messageCountListeners); + } + + /** + * Add a listener for MessageChanged events on this Folder.

+ *

+ * The implementation provided here adds this listener + * to an internal list of MessageChangedListeners. + * + * @param l the Listener for MessageChanged events + * @see MessageChangedEvent + */ + public synchronized void addMessageChangedListener(MessageChangedListener l) { + if (messageChangedListeners == null) + messageChangedListeners = new Vector<>(); + messageChangedListeners.addElement(l); + } + + /** + * Remove a MessageChanged listener.

+ *

+ * The implementation provided here removes this listener + * from the internal list of MessageChangedListeners. + * + * @param l the listener + * @see #addMessageChangedListener + */ + public synchronized void removeMessageChangedListener(MessageChangedListener l) { + if (messageChangedListeners != null) + messageChangedListeners.removeElement(l); + } + + /** + * Notify all MessageChangedListeners. Folder implementations are + * expected to use this method to broadcast MessageChanged events.

+ *

+ * The provided implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to registered + * MessageChangedListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the MessageChangedEvent type + * @param msg the message that changed + */ + protected void notifyMessageChangedListeners(int type, Message msg) { + if (messageChangedListeners == null) + return; + + MessageChangedEvent e = new MessageChangedEvent(this, type, msg); + queueEvent(e, messageChangedListeners); + } + + /* + * Add the event and vector of listeners to the queue to be delivered. + */ + private void queueEvent(MailEvent event, + Vector vector) { + /* + * Copy the vector in order to freeze the state of the set + * of EventListeners the event should be delivered to prior + * to delivery. This ensures that any changes made to the + * Vector from a target listener's method during the delivery + * of this event will not take effect until after the event is + * delivered. + */ + @SuppressWarnings("unchecked") + Vector v = (Vector) vector.clone(); + q.enqueue(event, v); + } + + /** + * override the default toString(), it will return the String + * from Folder.getFullName() or if that is null, it will use + * the default toString() behavior. + */ + + @Override + public String toString() { + String s = getFullName(); + if (s != null) + return s; + else + return super.toString(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/FolderClosedException.java b/net-mail/src/main/java/jakarta/mail/FolderClosedException.java new file mode 100644 index 0000000..89c458f --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/FolderClosedException.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * This exception is thrown when a method is invoked on a Messaging object + * and the Folder that owns that object has died due to some reason.

+ *

+ * Following the exception, the Folder is reset to the "closed" state. + * All messaging objects owned by the Folder should be considered invalid. + * The Folder can be reopened using the "open" method to reestablish the + * lost connection.

+ *

+ * The getMessage() method returns more detailed information about the + * error that caused this exception. + * + * @author John Mani + */ +@SuppressWarnings("serial") +public class FolderClosedException extends MessagingException { + transient private Folder folder; + + /** + * Constructs a FolderClosedException. + * + * @param folder The Folder + */ + public FolderClosedException(Folder folder) { + this(folder, null); + } + + /** + * Constructs a FolderClosedException with the specified + * detail message. + * + * @param folder The Folder + * @param message The detailed error message + */ + public FolderClosedException(Folder folder, String message) { + super(message); + this.folder = folder; + } + + /** + * Constructs a FolderClosedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public FolderClosedException(Folder folder, String message, Exception e) { + super(message, e); + this.folder = folder; + } + + /** + * Returns the dead Folder object + * + * @return the dead Folder object + */ + public Folder getFolder() { + return folder; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/FolderNotFoundException.java b/net-mail/src/main/java/jakarta/mail/FolderNotFoundException.java new file mode 100644 index 0000000..a2880e4 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/FolderNotFoundException.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * This exception is thrown by Folder methods, when those + * methods are invoked on a non existent folder. + * + * @author John Mani + */ +@SuppressWarnings("serial") +public class FolderNotFoundException extends MessagingException { + transient private Folder folder; + + /** + * Constructs a FolderNotFoundException with no detail message. + */ + public FolderNotFoundException() { + super(); + } + + /** + * Constructs a FolderNotFoundException. + * + * @param folder The Folder + * @since JavaMail 1.2 + */ + public FolderNotFoundException(Folder folder) { + super(); + this.folder = folder; + } + + /** + * Constructs a FolderNotFoundException with the specified + * detail message. + * + * @param folder The Folder + * @param s The detailed error message + * @since JavaMail 1.2 + */ + public FolderNotFoundException(Folder folder, String s) { + super(s); + this.folder = folder; + } + + /** + * Constructs a FolderNotFoundException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public FolderNotFoundException(Folder folder, String s, Exception e) { + super(s, e); + this.folder = folder; + } + + /** + * Constructs a FolderNotFoundException with the specified detail message + * and the specified folder. + * + * @param s The detail message + * @param folder The Folder + */ + public FolderNotFoundException(String s, Folder folder) { + super(s); + this.folder = folder; + } + + /** + * Returns the offending Folder object. + * + * @return the Folder object. Note that the returned value can be + * null. + */ + public Folder getFolder() { + return folder; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/Header.java b/net-mail/src/main/java/jakarta/mail/Header.java new file mode 100644 index 0000000..cddca3c --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Header.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import java.util.Objects; + +/** + * The Header class stores a name/value pair to represent headers. + * + * @author John Mani + */ + +public class Header { + + /** + * The name of the header. + * + * @since JavaMail 1.4 + */ + protected String name; + + /** + * The value of the header. + * + * @since JavaMail 1.4 + */ + protected String value; + + /** + * Construct a Header object. + * + * @param name name of the header + * @param value value of the header + */ + public Header(String name, String value) { + this.name = name; + this.value = value; + } + + /** + * Returns the name of this header. + * + * @return name of the header + */ + public String getName() { + return name; + } + + /** + * Returns the value of this header. + * + * @return value of the header + */ + public String getValue() { + return value; + } + + @Override + public int hashCode() { + return Objects.hash(name, value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } else if (obj == null) { + return false; + } else if (getClass() != obj.getClass()) { + return false; + } else { + Header other = (Header) obj; + return Objects.equals(name, other.getName()) && Objects.equals(value, other.getValue()); + } + } + +} diff --git a/net-mail/src/main/java/jakarta/mail/IllegalWriteException.java b/net-mail/src/main/java/jakarta/mail/IllegalWriteException.java new file mode 100644 index 0000000..ff452ec --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/IllegalWriteException.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + + +/** + * The exception thrown when a write is attempted on a read-only attribute + * of any Messaging object. + * + * @author John Mani + */ +@SuppressWarnings("serial") +public class IllegalWriteException extends MessagingException { + + /** + * Constructs an IllegalWriteException with no detail message. + */ + public IllegalWriteException() { + super(); + } + + /** + * Constructs an IllegalWriteException with the specified + * detail message. + * + * @param s The detailed error message + */ + public IllegalWriteException(String s) { + super(s); + } + + /** + * Constructs an IllegalWriteException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public IllegalWriteException(String s, Exception e) { + super(s, e); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/MailLogger.java b/net-mail/src/main/java/jakarta/mail/MailLogger.java new file mode 100644 index 0000000..07b20d5 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/MailLogger.java @@ -0,0 +1,432 @@ +/* + * Copyright (c) 2012, 2023 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 jakarta.mail; + +import java.io.PrintStream; +import java.text.MessageFormat; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 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. + */ + +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, 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/net-mail/src/main/java/jakarta/mail/MailSessionDefinition.java b/net-mail/src/main/java/jakarta/mail/MailSessionDefinition.java new file mode 100644 index 0000000..2ead5ec --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/MailSessionDefinition.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2012, 2023 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 jakarta.mail; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation used by Jakarta EE applications to define a MailSession + * to be registered with JNDI. The MailSession may be configured + * by setting the annotation elements for commonly used Session + * properties. Additional standard and vendor-specific properties may be + * specified using the properties element. + *

+ * The session will be registered under the name specified in the + * name element. It may be defined to be in any valid + * Jakarta EE namespace, and will determine the accessibility of + * the session from other components. + * + * @since JavaMail 1.5 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +@Repeatable(MailSessionDefinitions.class) +public @interface MailSessionDefinition { + + /** + * Description of this mail session. + * + * @return the description + */ + String description() default ""; + + /** + * JNDI name by which the mail session will be registered. + * + * @return the JNDI name + */ + String name(); + + /** + * Store protocol name. + * + * @return the store protocol name + */ + String storeProtocol() default ""; + + /** + * Transport protocol name. + * + * @return the transport protocol name + */ + String transportProtocol() default ""; + + /** + * Host name for the mail server. + * + * @return the host name + */ + String host() default ""; + + /** + * User name to use for authentication. + * + * @return the user name + */ + String user() default ""; + + /** + * Password to use for authentication. + * + * @return the password + */ + String password() default ""; + + /** + * From address for the user. + * + * @return the from address + */ + String from() default ""; + + /** + * Properties to include in the Session. + * Properties are specified using the format: + * propertyName=propertyValue with one property per array element. + * + * @return the properties + */ + String[] properties() default {}; +} diff --git a/net-mail/src/main/java/jakarta/mail/MailSessionDefinitions.java b/net-mail/src/main/java/jakarta/mail/MailSessionDefinitions.java new file mode 100644 index 0000000..cae5653 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/MailSessionDefinitions.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2012, 2023 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 jakarta.mail; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Declares one or more MailSessionDefinition annotations. + * + * @see MailSessionDefinition + * @since JavaMail 1.5 + */ +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface MailSessionDefinitions { + MailSessionDefinition[] value(); +} diff --git a/net-mail/src/main/java/jakarta/mail/Message.java b/net-mail/src/main/java/jakarta/mail/Message.java new file mode 100644 index 0000000..13d1ebb --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Message.java @@ -0,0 +1,678 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import jakarta.mail.search.SearchTerm; +import java.util.Date; + +/** + * This class models an email message. This is an abstract class. + * Subclasses provide actual implementations.

+ *

+ * Message implements the Part interface. Message contains a set of + * attributes and a "content". Messages within a folder also have a + * set of flags that describe its state within the folder.

+ *

+ * Message defines some new attributes in addition to those defined + * in the Part interface. These attributes specify meta-data + * for the message - i.e., addressing and descriptive information about + * the message.

+ *

+ * Message objects are obtained either from a Folder or by constructing + * a new Message object of the appropriate subclass. Messages that have + * been received are normally retrieved from a folder named "INBOX".

+ *

+ * A Message object obtained from a folder is just a lightweight + * reference to the actual message. The Message is 'lazily' filled + * up (on demand) when each item is requested from the message. Note + * that certain folder implementations may return Message objects that + * are pre-filled with certain user-specified items. + *

+ * To send a message, an appropriate subclass of Message (e.g., + * MimeMessage) is instantiated, the attributes and content are + * filled in, and the message is sent using the Transport.send + * method. + * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + * @see Part + */ + +public abstract class Message implements Part { + + /** + * The number of this message within its folder, or zero if + * the message was not retrieved from a folder. + */ + protected int msgnum = 0; + + /** + * True if this message has been expunged. + */ + protected boolean expunged = false; + + /** + * The containing folder, if this message is obtained from a folder + */ + protected Folder folder = null; + + /** + * The Session object for this Message + */ + protected Session session = null; + + /** + * No-arg version of the constructor. + */ + protected Message() { + } + + /** + * Constructor that takes a Folder and a message number. + * Used by Folder implementations. + * + * @param folder containing folder + * @param msgnum this message's sequence number within this folder + */ + protected Message(Folder folder, int msgnum) { + this.folder = folder; + this.msgnum = msgnum; + session = folder.store.session; + } + + /** + * Constructor that takes a Session. Used for client created + * Message objects. + * + * @param session A Session object + */ + protected Message(Session session) { + this.session = session; + } + + /** + * Return the Session used when this message was created. + * + * @return the message's Session + * @since JavaMail 1.5 + */ + public Session getSession() { + return session; + } + + /** + * Returns the "From" attribute. The "From" attribute contains + * the identity of the person(s) who wished this message to + * be sent.

+ *

+ * In certain implementations, this may be different + * from the entity that actually sent the message.

+ *

+ * This method returns null if this attribute + * is not present in this message. Returns an empty array if + * this attribute is present, but contains no addresses. + * + * @return array of Address objects + * @throws MessagingException for failures + */ + public abstract Address[] getFrom() throws MessagingException; + + /** + * Set the "From" attribute in this Message. + * + * @param address the sender + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + public abstract void setFrom(Address address) + throws MessagingException; + + /** + * Set the "From" attribute in this Message. The value of this + * attribute is obtained from the property "mail.user". If this + * property is absent, the system property "user.name" is used. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + public abstract void setFrom() throws MessagingException; + + /** + * Add these addresses to the existing "From" attribute + * + * @param addresses the senders + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + public abstract void addFrom(Address[] addresses) + throws MessagingException; + + /** + * Get all the recipient addresses of the given type.

+ *

+ * This method returns null if no recipients of + * the given type are present in this message. It may return an + * empty array if the header is present, but contains no addresses. + * + * @param type the recipient type + * @return array of Address objects + * @throws MessagingException for failures + * @see RecipientType#TO + * @see RecipientType#CC + * @see RecipientType#BCC + */ + public abstract Address[] getRecipients(RecipientType type) + throws MessagingException; + + /** + * Get all the recipient addresses for the message. + * The default implementation extracts the TO, CC, and BCC + * recipients using the getRecipients method.

+ *

+ * This method returns null if none of the recipient + * headers are present in this message. It may Return an empty array + * if any recipient header is present, but contains no addresses. + * + * @return array of Address objects + * @throws MessagingException for failures + * @see RecipientType#TO + * @see RecipientType#CC + * @see RecipientType#BCC + * @see #getRecipients + */ + public Address[] getAllRecipients() throws MessagingException { + Address[] to = getRecipients(RecipientType.TO); + Address[] cc = getRecipients(RecipientType.CC); + Address[] bcc = getRecipients(RecipientType.BCC); + + if (cc == null && bcc == null) + return to; // a common case + + int numRecip = + (to != null ? to.length : 0) + + (cc != null ? cc.length : 0) + + (bcc != null ? bcc.length : 0); + Address[] addresses = new Address[numRecip]; + int pos = 0; + if (to != null) { + System.arraycopy(to, 0, addresses, pos, to.length); + pos += to.length; + } + if (cc != null) { + System.arraycopy(cc, 0, addresses, pos, cc.length); + pos += cc.length; + } + if (bcc != null) { + System.arraycopy(bcc, 0, addresses, pos, bcc.length); + // pos += bcc.length; + } + return addresses; + } + + /** + * Set the recipient addresses. All addresses of the specified + * type are replaced by the addresses parameter. + * + * @param type the recipient type + * @param addresses the addresses + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + public abstract void setRecipients(RecipientType type, Address[] addresses) + throws MessagingException; + + /** + * Set the recipient address. All addresses of the specified + * type are replaced by the address parameter.

+ *

+ * The default implementation uses the setRecipients method. + * + * @param type the recipient type + * @param address the address + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws MessagingException for other failures + */ + public void setRecipient(RecipientType type, Address address) + throws MessagingException { + if (address == null) + setRecipients(type, null); + else { + Address[] a = new Address[1]; + a[0] = address; + setRecipients(type, a); + } + } + + /** + * Add these recipient addresses to the existing ones of the given type. + * + * @param type the recipient type + * @param addresses the addresses + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + public abstract void addRecipients(RecipientType type, Address[] addresses) + throws MessagingException; + + /** + * Add this recipient address to the existing ones of the given type.

+ *

+ * The default implementation uses the addRecipients method. + * + * @param type the recipient type + * @param address the address + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws MessagingException for other failures + */ + public void addRecipient(RecipientType type, Address address) + throws MessagingException { + Address[] a = new Address[1]; + a[0] = address; + addRecipients(type, a); + } + + /** + * Get the addresses to which replies should be directed. + * This will usually be the sender of the message, but + * some messages may direct replies to a different address.

+ *

+ * The default implementation simply calls the getFrom + * method.

+ *

+ * This method returns null if the corresponding + * header is not present. Returns an empty array if the header + * is present, but contains no addresses. + * + * @return addresses to which replies should be directed + * @throws MessagingException for failures + * @see #getFrom + */ + public Address[] getReplyTo() throws MessagingException { + return getFrom(); + } + + /** + * Set the addresses to which replies should be directed. + * (Normally only a single address will be specified.) + * Not all message types allow this to be specified separately + * from the sender of the message.

+ *

+ * The default implementation provided here just throws the + * MethodNotSupportedException. + * + * @param addresses addresses to which replies should be directed + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MethodNotSupportedException if the underlying + * implementation does not support setting this + * attribute + * @throws MessagingException for other failures + */ + public void setReplyTo(Address[] addresses) throws MessagingException { + throw new MethodNotSupportedException("setReplyTo not supported"); + } + + /** + * Get the subject of this message. + * + * @return the subject + * @throws MessagingException for failures + */ + public abstract String getSubject() throws MessagingException; + + /** + * Set the subject of this message. + * + * @param subject the subject + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + public abstract void setSubject(String subject) + throws MessagingException; + + /** + * Get the date this message was sent. + * + * @return the date this message was sent + * @throws MessagingException for failures + */ + public abstract Date getSentDate() throws MessagingException; + + /** + * Set the sent date of this message. + * + * @param date the sent date of this message + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + public abstract void setSentDate(Date date) throws MessagingException; + + /** + * Get the date this message was received. + * + * @return the date this message was received + * @throws MessagingException for failures + */ + public abstract Date getReceivedDate() throws MessagingException; + + /** + * Returns a Flags object containing the flags for + * this message.

+ *

+ * Modifying any of the flags in this returned Flags object will + * not affect the flags of this message. Use setFlags() + * to do that. + * + * @return Flags object containing the flags for this message + * @throws MessagingException for failures + * @see Flags + * @see #setFlags + */ + public abstract Flags getFlags() throws MessagingException; + + /** + * Check whether the flag specified in the flag + * argument is set in this message.

+ *

+ * The default implementation uses getFlags. + * + * @param flag the flag + * @return value of the specified flag for this message + * @throws MessagingException for failures + * @see Flags.Flag#ANSWERED + * @see Flags.Flag#DELETED + * @see Flags.Flag#DRAFT + * @see Flags.Flag#FLAGGED + * @see Flags.Flag#RECENT + * @see Flags.Flag#SEEN + * @see Flags.Flag + */ + public boolean isSet(Flags.Flag flag) throws MessagingException { + return getFlags().contains(flag); + } + + /** + * Set the specified flags on this message to the specified value. + * Note that any flags in this message that are not specified in + * the given Flags object are unaffected.

+ *

+ * This will result in a MessageChangedEvent being + * delivered to any MessageChangedListener registered on this + * Message's containing folder. + * + * @param flag Flags object containing the flags to be set + * @param set the value to be set + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values. + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + * @see jakarta.mail.event.MessageChangedEvent + */ + public abstract void setFlags(Flags flag, boolean set) + throws MessagingException; + + /** + * Set the specified flag on this message to the specified value. + *

+ * This will result in a MessageChangedEvent being + * delivered to any MessageChangedListener registered on this + * Message's containing folder.

+ *

+ * The default implementation uses the setFlags method. + * + * @param flag Flags.Flag object containing the flag to be set + * @param set the value to be set + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values. + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + * @see jakarta.mail.event.MessageChangedEvent + */ + public void setFlag(Flags.Flag flag, boolean set) + throws MessagingException { + Flags f = new Flags(flag); + setFlags(f, set); + } + + /** + * Get the Message number for this Message. + * A Message object's message number is the relative + * position of this Message in its Folder. Note that the message + * number for a particular Message can change during a session + * if other messages in the Folder are deleted and expunged.

+ *

+ * Valid message numbers start at 1. Messages that do not belong + * to any folder (like newly composed or derived messages) have 0 + * as their message number. + * + * @return the message number + */ + public int getMessageNumber() { + return msgnum; + } + + /** + * Set the Message number for this Message. This method is + * invoked only by the implementation classes. + * + * @param msgnum the message number + */ + protected void setMessageNumber(int msgnum) { + this.msgnum = msgnum; + } + + /** + * Get the folder from which this message was obtained. If + * this is a new message or nested message, this method returns + * null. + * + * @return the containing folder + */ + public Folder getFolder() { + return folder; + } + + /** + * Checks whether this message is expunged. All other methods except + * getMessageNumber() are invalid on an expunged + * Message object.

+ *

+ * Messages that are expunged due to an explict expunge() + * request on the containing Folder are removed from the Folder + * immediately. Messages that are externally expunged by another source + * are marked "expunged" and return true for the isExpunged() method, + * but they are not removed from the Folder until an explicit + * expunge() is done on the Folder.

+ *

+ * See the description of expunge() for more details on + * expunge handling. + * + * @return true if the message is expunged + * @see Folder#expunge + */ + public boolean isExpunged() { + return expunged; + } + + /** + * Sets the expunged flag for this Message. This method is to + * be used only by the implementation classes. + * + * @param expunged the expunged flag + */ + protected void setExpunged(boolean expunged) { + this.expunged = expunged; + } + + /** + * Get a new Message suitable for a reply to this message. + * The new Message will have its attributes and headers + * set up appropriately. Note that this new message object + * will be empty, that is, it will not have a "content". + * These will have to be suitably filled in by the client.

+ *

+ * If replyToAll is set, the new Message will be addressed + * to all recipients of this message. Otherwise, the reply will be + * addressed to only the sender of this message (using the value + * of the getReplyTo method).

+ *

+ * The "Subject" field is filled in with the original subject + * prefixed with "Re:" (unless it already starts with "Re:").

+ *

+ * The reply message will use the same session as this message. + * + * @param replyToAll reply should be sent to all recipients + * of this message + * @return the reply Message + * @throws MessagingException for failures + */ + public abstract Message reply(boolean replyToAll) throws MessagingException; + + /** + * Save any changes made to this message into the message-store + * when the containing folder is closed, if the message is contained + * in a folder. (Some implementations may save the changes + * immediately.) Update any header fields to be consistent with the + * changed message contents. If any part of a message's headers or + * contents are changed, saveChanges must be called to ensure that + * those changes are permanent. If saveChanges is not called, any + * such modifications may or may not be saved, depending on the + * message store and folder implementation.

+ *

+ * Messages obtained from folders opened READ_ONLY should not be + * modified and saveChanges should not be called on such messages. + * + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values. + * @throws MessagingException for other failures + */ + public abstract void saveChanges() throws MessagingException; + + /** + * Apply the specified Search criterion to this message. + * + * @param term the Search criterion + * @return true if the Message matches this search + * criterion, false otherwise. + * @throws MessagingException for failures + * @see SearchTerm + */ + public boolean match(SearchTerm term) throws MessagingException { + return term.match(this); + } + + /** + * This inner class defines the types of recipients allowed by + * the Message class. The currently defined types are TO, + * CC and BCC. + *

+ * Note that this class only has a protected constructor, thereby + * restricting new Recipient types to either this class or subclasses. + * This effectively implements an enumeration of the allowed Recipient + * types. + *

+ * The following code sample shows how to use this class to obtain + * the "TO" recipients from a message. + *

+     *
+     * Message msg = folder.getMessages(1);
+     * Address[] a = m.getRecipients(Message.RecipientType.TO);
+     *
+     * 
+ * + * @see Message#getRecipients + * @see Message#setRecipients + * @see Message#addRecipients + */ + public static class RecipientType { + /** + * The "To" (primary) recipients. + */ + public static final RecipientType TO = new RecipientType("To"); + /** + * The "Cc" (carbon copy) recipients. + */ + public static final RecipientType CC = new RecipientType("Cc"); + /** + * The "Bcc" (blind carbon copy) recipients. + */ + public static final RecipientType BCC = new RecipientType("Bcc"); + + /** + * The type of recipient, usually the name of a corresponding + * Internet standard header. + */ + protected String type; + + /** + * Constructor for use by subclasses. + * + * @param type the recipient type + */ + protected RecipientType(String type) { + this.type = type; + } + + @Override + public String toString() { + return type; + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/MessageAware.java b/net-mail/src/main/java/jakarta/mail/MessageAware.java new file mode 100644 index 0000000..f3b5a3a --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/MessageAware.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * An interface optionally implemented by DataSources to + * supply information to a DataContentHandler about the + * message context in which the data content object is operating. + * + * @see MessageContext + * @see jakarta.activation.DataSource + * @see jakarta.activation.DataContentHandler + * @since JavaMail 1.1 + */ +public interface MessageAware { + /** + * Return the message context. + * + * @return the message context + */ + MessageContext getMessageContext(); +} diff --git a/net-mail/src/main/java/jakarta/mail/MessageContext.java b/net-mail/src/main/java/jakarta/mail/MessageContext.java new file mode 100644 index 0000000..c8d1555 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/MessageContext.java @@ -0,0 +1,100 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * The context in which a piece of Message content is contained. A + * MessageContext object is returned by the + * getMessageContext method of the + * MessageAware interface. MessageAware is + * typically implemented by DataSources to allow a + * DataContentHandler to pass on information about the + * context in which a data content object is operating. + * + * @see MessageAware + * @see jakarta.activation.DataSource + * @see jakarta.activation.DataContentHandler + * @since JavaMail 1.1 + */ +public class MessageContext { + private Part part; + + /** + * Create a MessageContext object describing the context of the given Part. + * + * @param part the Part + */ + public MessageContext(Part part) { + this.part = part; + } + + /** + * Return the Message containing an arbitrary Part. + * Follows the parent chain up through containing Multipart + * objects until it comes to a Message object, or null. + * + * @return the containing Message, or null if none + * @see BodyPart#getParent + * @see Multipart#getParent + */ + private static Message getMessage(Part p) throws MessagingException { + while (p != null) { + if (p instanceof Message) + return (Message) p; + BodyPart bp = (BodyPart) p; + Multipart mp = bp.getParent(); + if (mp == null) // MimeBodyPart might not be in a MimeMultipart + return null; + p = mp.getParent(); + } + return null; + } + + /** + * Return the Part that contains the content. + * + * @return the containing Part, or null if not known + */ + public Part getPart() { + return part; + } + + /** + * Return the Message that contains the content. + * Follows the parent chain up through containing Multipart + * objects until it comes to a Message object, or null. + * + * @return the containing Message, or null if not known + */ + public Message getMessage() { + try { + return getMessage(part); + } catch (MessagingException ex) { + return null; + } + } + + /** + * Return the Session we're operating in. + * + * @return the Session, or null if not known + */ + public Session getSession() { + Message msg = getMessage(); + return msg != null ? msg.getSession() : null; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/MessageRemovedException.java b/net-mail/src/main/java/jakarta/mail/MessageRemovedException.java new file mode 100644 index 0000000..acc394b --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/MessageRemovedException.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * The exception thrown when an invalid method is invoked on an expunged + * Message. The only valid methods on an expunged Message are + * isExpunged() and getMessageNumber(). + * + * @author John Mani + * @see Message#isExpunged() + * @see Message#getMessageNumber() + */ +@SuppressWarnings("serial") +public class MessageRemovedException extends MessagingException { + + /** + * Constructs a MessageRemovedException with no detail message. + */ + public MessageRemovedException() { + super(); + } + + /** + * Constructs a MessageRemovedException with the specified + * detail message. + * + * @param s The detailed error message + */ + public MessageRemovedException(String s) { + super(s); + } + + /** + * Constructs a MessageRemovedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public MessageRemovedException(String s, Exception e) { + super(s, e); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/MessagingException.java b/net-mail/src/main/java/jakarta/mail/MessagingException.java new file mode 100644 index 0000000..5a438a5 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/MessagingException.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * The base class for all exceptions thrown by the Messaging classes + * + * @author John Mani + * @author Bill Shannon + */ +@SuppressWarnings("serial") +public class MessagingException extends Exception { + + /** + * The next exception in the chain. + * + * @serial + */ + private Exception next; + + /** + * Constructs a MessagingException with no detail message. + */ + public MessagingException() { + super(); + } + + /** + * Constructs a MessagingException with the specified detail message. + * + * @param s the detail message + */ + public MessagingException(String s) { + super(s); + } + + /** + * Constructs a MessagingException with the specified + * Exception and detail message. The specified exception is chained + * to this exception. + * + * @param s the detail message + * @param e the embedded exception + * @see #getNextException + * @see #setNextException + * @see #getCause + */ + public MessagingException(String s, Exception e) { + super(s); + next = e; + } + + /** + * Get the next exception chained to this one. If the + * next exception is a MessagingException, the chain + * may extend further. + * + * @return next Exception, null if none. + */ + public synchronized Exception getNextException() { + return next; + } + + /** + * Overrides the getCause method of Throwable + * to return the next exception in the chain of nested exceptions. + * + * @return next Exception, null if none. + */ + @Override + public synchronized Throwable getCause() { + return next; + } + + /** + * Add an exception to the end of the chain. If the end + * is not a MessagingException, this + * exception cannot be added to the end. + * + * @param ex the new end of the Exception chain + * @return true if this Exception + * was added, false otherwise. + */ + public synchronized boolean setNextException(Exception ex) { + Exception theEnd = this; + while (theEnd instanceof MessagingException && + ((MessagingException) theEnd).next != null) { + theEnd = ((MessagingException) theEnd).next; + } + // If the end is a MessagingException, we can add this + // exception to the chain. + if (theEnd instanceof MessagingException) { + ((MessagingException) theEnd).next = ex; + return true; + } else + return false; + } + + /** + * Override toString method to provide information on + * nested exceptions. + */ + @Override + public synchronized String toString() { + String s = super.toString(); + Exception n = next; + if (n == null) + return s; + StringBuilder sb = new StringBuilder(s == null ? "" : s); + while (n != null) { + sb.append(";\n nested exception is:\n\t"); + if (n instanceof MessagingException) { + MessagingException mex = (MessagingException) n; + sb.append(mex.superToString()); + n = mex.next; + } else { + sb.append(n); + n = null; + } + } + return sb.toString(); + } + + /** + * Return the "toString" information for this exception, + * without any information on nested exceptions. + */ + private final String superToString() { + return super.toString(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/MethodNotSupportedException.java b/net-mail/src/main/java/jakarta/mail/MethodNotSupportedException.java new file mode 100644 index 0000000..3ae8456 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/MethodNotSupportedException.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + + +/** + * The exception thrown when a method is not supported by the + * implementation + * + * @author John Mani + */ +@SuppressWarnings("serial") +public class MethodNotSupportedException extends MessagingException { + + /** + * Constructs a MethodNotSupportedException with no detail message. + */ + public MethodNotSupportedException() { + super(); + } + + /** + * Constructs a MethodNotSupportedException with the specified + * detail message. + * + * @param s The detailed error message + */ + public MethodNotSupportedException(String s) { + super(s); + } + + /** + * Constructs a MethodNotSupportedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param s The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public MethodNotSupportedException(String s, Exception e) { + super(s, e); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/Multipart.java b/net-mail/src/main/java/jakarta/mail/Multipart.java new file mode 100644 index 0000000..fa799ca --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Multipart.java @@ -0,0 +1,265 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import jakarta.mail.util.StreamProvider; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Vector; + +/** + * Multipart is a container that holds multiple body parts. Multipart + * provides methods to retrieve and set its subparts.

+ *

+ * Multipart also acts as the base class for the content object returned + * by most Multipart DataContentHandlers. For example, invoking getContent() + * on a DataHandler whose source is a "multipart/signed" data source may + * return an appropriate subclass of Multipart.

+ *

+ * Some messaging systems provide different subtypes of Multiparts. For + * example, MIME specifies a set of subtypes that include "alternative", + * "mixed", "related", "parallel", "signed", etc.

+ *

+ * Multipart is an abstract class. Subclasses provide actual implementations. + * + * @author John Mani + */ + +public abstract class Multipart { + + /** + * Instance of stream provider. + * + * @since JavaMail 2.1 + */ + protected final StreamProvider streamProvider = StreamProvider.provider(); + /** + * Vector of BodyPart objects. + */ + protected Vector parts = new Vector<>(); // Holds BodyParts + /** + * This field specifies the content-type of this multipart + * object. It defaults to "multipart/mixed". + */ + protected String contentType = "multipart/mixed"; // Content-Type + /** + * The Part containing this Multipart, + * if known. + * + * @since JavaMail 1.1 + */ + protected Part parent; + + /** + * Default constructor. An empty Multipart object is created. + */ + protected Multipart() { + } + + /** + * Setup this Multipart object from the given MultipartDataSource.

+ *

+ * The method adds the MultipartDataSource's BodyPart + * objects into this Multipart. This Multipart's contentType is + * set to that of the MultipartDataSource.

+ *

+ * This method is typically used in those cases where one + * has a multipart data source that has already been pre-parsed into + * the individual body parts (for example, an IMAP datasource), but + * needs to create an appropriate Multipart subclass that represents + * a specific multipart subtype. + * + * @param mp Multipart datasource + * @throws MessagingException for failures + */ + protected synchronized void setMultipartDataSource(MultipartDataSource mp) + throws MessagingException { + contentType = mp.getContentType(); + + int count = mp.getCount(); + for (int i = 0; i < count; i++) + addBodyPart(mp.getBodyPart(i)); + } + + /** + * Return the content-type of this Multipart.

+ *

+ * This implementation just returns the value of the + * contentType field. + * + * @return content-type + * @see #contentType + */ + public synchronized String getContentType() { + return contentType; + } + + /** + * Return the number of enclosed BodyPart objects. + * + * @return number of parts + * @throws MessagingException for failures + * @see #parts + */ + public synchronized int getCount() throws MessagingException { + if (parts == null) + return 0; + + return parts.size(); + } + + /** + * Get the specified Part. Parts are numbered starting at 0. + * + * @param index the index of the desired Part + * @return the Part + * @throws IndexOutOfBoundsException if the given index + * is out of range. + * @throws MessagingException for other failures + */ + public synchronized BodyPart getBodyPart(int index) + throws MessagingException { + if (parts == null) + throw new IndexOutOfBoundsException("No such BodyPart"); + + return parts.elementAt(index); + } + + /** + * Remove the specified part from the multipart message. + * Shifts all the parts after the removed part down one. + * + * @param part The part to remove + * @return true if part removed, false otherwise + * @throws MessagingException if no such Part exists + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + */ + public synchronized boolean removeBodyPart(BodyPart part) + throws MessagingException { + if (parts == null) + throw new MessagingException("No such body part"); + + boolean ret = parts.removeElement(part); + part.setParent(null); + return ret; + } + + /** + * Remove the part at specified location (starting from 0). + * Shifts all the parts after the removed part down one. + * + * @param index Index of the part to remove + * @throws IndexOutOfBoundsException if the given index + * is out of range. + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws MessagingException for other failures + */ + public synchronized void removeBodyPart(int index) + throws MessagingException { + if (parts == null) + throw new IndexOutOfBoundsException("No such BodyPart"); + + BodyPart part = parts.elementAt(index); + parts.removeElementAt(index); + part.setParent(null); + } + + /** + * Adds a Part to the multipart. The BodyPart is appended to + * the list of existing Parts. + * + * @param part The Part to be appended + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws MessagingException for other failures + */ + public synchronized void addBodyPart(BodyPart part) + throws MessagingException { + if (parts == null) + parts = new Vector<>(); + + parts.addElement(part); + part.setParent(this); + } + + /** + * Adds a BodyPart at position index. + * If index is not the last one in the list, + * the subsequent parts are shifted up. If index + * is larger than the number of parts present, the + * BodyPart is appended to the end. + * + * @param part The BodyPart to be inserted + * @param index Location where to insert the part + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws MessagingException for other failures + */ + public synchronized void addBodyPart(BodyPart part, int index) + throws MessagingException { + if (parts == null) + parts = new Vector<>(); + + parts.insertElementAt(part, index); + part.setParent(this); + } + + /** + * Output an appropriately encoded bytestream to the given + * OutputStream. The implementation subclass decides the + * appropriate encoding algorithm to be used. The bytestream + * is typically used for sending. + * + * @param os the stream to write to + * @throws IOException if an IO related exception occurs + * @throws MessagingException for other failures + */ + public abstract void writeTo(OutputStream os) + throws IOException, MessagingException; + + /** + * Return the Part that contains this Multipart + * object, or null if not known. + * + * @return the parent Part + * @since JavaMail 1.1 + */ + public synchronized Part getParent() { + return parent; + } + + /** + * Set the parent of this Multipart to be the specified + * Part. Normally called by the Message + * or BodyPart setContent(Multipart) method. + * parent may be null if the + * Multipart is being removed from its containing + * Part. + * + * @param parent the parent Part + * @since JavaMail 1.1 + */ + public synchronized void setParent(Part parent) { + this.parent = parent; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/MultipartDataSource.java b/net-mail/src/main/java/jakarta/mail/MultipartDataSource.java new file mode 100644 index 0000000..02a9355 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/MultipartDataSource.java @@ -0,0 +1,58 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import jakarta.activation.DataSource; + +/** + * MultipartDataSource is a DataSource that contains body + * parts. This allows "mail aware" DataContentHandlers to + * be implemented more efficiently by being aware of such + * DataSources and using the appropriate methods to access + * BodyParts.

+ *

+ * Note that the data of a MultipartDataSource is also available as + * an input stream.

+ *

+ * This interface will typically be implemented by providers that + * preparse multipart bodies, for example an IMAP provider. + * + * @author John Mani + * @see jakarta.activation.DataSource + */ + +public interface MultipartDataSource extends DataSource { + + /** + * Return the number of enclosed BodyPart objects. + * + * @return number of parts + */ + int getCount(); + + /** + * Get the specified Part. Parts are numbered starting at 0. + * + * @param index the index of the desired Part + * @return the Part + * @throws IndexOutOfBoundsException if the given index + * is out of range. + * @throws MessagingException for other failures + */ + BodyPart getBodyPart(int index) throws MessagingException; + +} diff --git a/net-mail/src/main/java/jakarta/mail/NoSuchProviderException.java b/net-mail/src/main/java/jakarta/mail/NoSuchProviderException.java new file mode 100644 index 0000000..a5a1013 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/NoSuchProviderException.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * This exception is thrown when Session attempts to instantiate a + * Provider that doesn't exist. + * + * @author Max Spivak + */ +@SuppressWarnings("serial") +public class NoSuchProviderException extends MessagingException { + + /** + * Constructs a NoSuchProviderException with no detail message. + */ + public NoSuchProviderException() { + super(); + } + + /** + * Constructs a NoSuchProviderException with the specified + * detail message. + * + * @param message The detailed error message + */ + public NoSuchProviderException(String message) { + super(message); + } + + /** + * Constructs a NoSuchProviderException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public NoSuchProviderException(String message, Exception e) { + super(message, e); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/Part.java b/net-mail/src/main/java/jakarta/mail/Part.java new file mode 100644 index 0000000..aeadd21 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Part.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import jakarta.activation.DataHandler; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; + +/** + * The Part interface is the common base interface for + * Messages and BodyParts.

+ *

+ * Part consists of a set of attributes and a "Content".

+ * + * Attributes:

+ *

+ * The Jakarta Mail API defines a set of standard Part attributes that are + * considered to be common to most existing Mail systems. These + * attributes have their own settor and gettor methods. Mail systems + * may support other Part attributes as well, these are represented as + * name-value pairs where both the name and value are Strings.

+ * + * Content:

+ *

+ * The data type of the "content" is returned by + * the getContentType() method. The MIME typing system + * is used to name data types.

+ *

+ * The "content" of a Part is available in various formats: + *

    + *
  • As a DataHandler - using the getDataHandler() method. + * The "content" of a Part is also available through a + * jakarta.activation.DataHandler object. The DataHandler + * object allows clients to discover the operations available on the + * content, and to instantiate the appropriate component to perform + * those operations. + * + *
  • As an input stream - using the getInputStream() method. + * Any mail-specific encodings are decoded before this stream is returned. + * + *
  • As a Java object - using the getContent() method. + * This method returns the "content" as a Java object. + * The returned object is of course dependent on the content + * itself. In particular, a "multipart" Part's content is always a + * Multipart or subclass thereof. That is, getContent() on a + * "multipart" type Part will always return a Multipart (or subclass) object. + *
+ *

+ * Part provides the writeTo() method that streams + * out its bytestream in mail-safe form suitable for transmission. + * This bytestream is typically an aggregation of the Part attributes + * and its content's bytestream.

+ *

+ * Message and BodyPart implement the Part interface. Note that in + * MIME parlance, Part models an Entity (RFC 2045, Section 2.4). + * + * @author John Mani + */ + +public interface Part { + + /** + * This part should be presented as an attachment. + * + * @see #getDisposition + * @see #setDisposition + */ + String ATTACHMENT = "attachment"; + /** + * This part should be presented inline. + * + * @see #getDisposition + * @see #setDisposition + */ + String INLINE = "inline"; + + /** + * Return the size of the content of this part in bytes. + * Return -1 if the size cannot be determined.

+ *

+ * Note that the size may not be an exact measure of the content + * size and may or may not account for any transfer encoding + * of the content. The size is appropriate for display in a + * user interface to give the user a rough idea of the size + * of this part. + * + * @return size of content in bytes + * @throws MessagingException for failures + */ + int getSize() throws MessagingException; + + /** + * Return the number of lines in the content of this part. + * Return -1 if the number cannot be determined. + *

+ * Note that this number may not be an exact measure of the + * content length and may or may not account for any transfer + * encoding of the content. + * + * @return number of lines in the content. + * @throws MessagingException for failures + */ + int getLineCount() throws MessagingException; + + /** + * Returns the Content-Type of the content of this part. + * Returns null if the Content-Type could not be determined.

+ *

+ * The MIME typing system is used to name Content-types. + * + * @return The ContentType of this part + * @throws MessagingException for failures + * @see jakarta.activation.DataHandler + */ + String getContentType() throws MessagingException; + + /** + * Is this Part of the specified MIME type? This method + * compares only the primaryType and + * subType. + * The parameters of the content types are ignored.

+ *

+ * For example, this method will return true when + * comparing a Part of content type "text/plain" + * with "text/plain; charset=foobar".

+ *

+ * If the subType of mimeType is the + * special character '*', then the subtype is ignored during the + * comparison. + * + * @param mimeType the MIME type to test + * @return true if this part is of the specified type + * @throws MessagingException for failures + */ + boolean isMimeType(String mimeType) throws MessagingException; + + /** + * Return the disposition of this part. The disposition + * describes how the part should be presented to the user. + * (See RFC 2183.) The return value should be considered + * without regard to case. For example: + *

+     * String disp = part.getDisposition();
+     * if (disp == null || disp.equalsIgnoreCase(Part.ATTACHMENT))
+     * 	// treat as attachment if not first part
+     * 
+ * + * @return disposition of this part, or null if unknown + * @throws MessagingException for failures + * @see #ATTACHMENT + * @see #INLINE + * @see #getFileName + */ + String getDisposition() throws MessagingException; + + /** + * Set the disposition of this part. + * + * @param disposition disposition of this part + * @throws IllegalWriteException if the underlying implementation + * does not support modification of this header + * @throws IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @throws MessagingException for other failures + * @see #ATTACHMENT + * @see #INLINE + * @see #setFileName + */ + void setDisposition(String disposition) throws MessagingException; + + /** + * Return a description String for this part. This typically + * associates some descriptive information with this part. + * Returns null if none is available. + * + * @return description of this part + * @throws MessagingException for failures + */ + String getDescription() throws MessagingException; + + /** + * Set a description String for this part. This typically + * associates some descriptive information with this part. + * + * @param description description of this part + * @throws IllegalWriteException if the underlying implementation + * does not support modification of this header + * @throws IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @throws MessagingException for other failures + */ + void setDescription(String description) throws MessagingException; + + /** + * Get the filename associated with this part, if possible. + * Useful if this part represents an "attachment" that was + * loaded from a file. The filename will usually be a simple + * name, not including directory components. + * + * @return Filename to associate with this part + * @throws MessagingException for failures + */ + String getFileName() throws MessagingException; + + /** + * Set the filename associated with this part, if possible. + * Useful if this part represents an "attachment" that was + * loaded from a file. The filename will usually be a simple + * name, not including directory components. + * + * @param filename Filename to associate with this part + * @throws IllegalWriteException if the underlying implementation + * does not support modification of this header + * @throws IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @throws MessagingException for other failures + */ + void setFileName(String filename) throws MessagingException; + + /** + * Return an input stream for this part's "content". Any + * mail-specific transfer encodings will be decoded before the + * input stream is provided.

+ *

+ * This is typically a convenience method that just invokes + * the DataHandler's getInputStream() method. + * + * @return an InputStream + * @throws IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * jakarta.activation.DataHandler for more details. + * @throws MessagingException for other failures + * @see #getDataHandler + * @see jakarta.activation.DataHandler#getInputStream + */ + InputStream getInputStream() + throws IOException, MessagingException; + + /** + * Return a DataHandler for the content within this part. The + * DataHandler allows clients to operate on as well as retrieve + * the content. + * + * @return DataHandler for the content + * @throws MessagingException for failures + */ + DataHandler getDataHandler() throws MessagingException; + + /** + * This method provides the mechanism to set this part's content. + * The DataHandler wraps around the actual content. + * + * @param dh The DataHandler for the content. + * @throws IllegalWriteException if the underlying implementation + * does not support modification of existing values + * @throws IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @throws MessagingException for other failures + */ + void setDataHandler(DataHandler dh) throws MessagingException; + + /** + * Return the content as a Java object. The type of the returned + * object is of course dependent on the content itself. For example, + * the object returned for "text/plain" content is usually a String + * object. The object returned for a "multipart" content is always a + * Multipart subclass. For content-types that are unknown to the + * DataHandler system, an input stream is returned as the content

+ *

+ * This is a convenience method that just invokes the DataHandler's + * getContent() method + * + * @return Object + * @throws IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * jakarta.activation.DataHandler for more details. + * @throws MessagingException for other failures + * @see jakarta.activation.DataHandler#getContent + */ + Object getContent() throws IOException, MessagingException; + + /** + * This method sets the given Multipart object as this message's + * content. + * + * @param mp The multipart object that is the Message's content + * @throws IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @throws IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @throws MessagingException for other failures + */ + void setContent(Multipart mp) throws MessagingException; + + /** + * A convenience method for setting this part's content. The part + * internally wraps the content in a DataHandler.

+ *

+ * Note that a DataContentHandler class for the specified type should + * be available to the Jakarta Mail implementation for this to work right. + * i.e., to do setContent(foobar, "application/x-foobar"), + * a DataContentHandler for "application/x-foobar" should be installed. + * Refer to the Java Activation Framework for more information. + * + * @param obj A java object. + * @param type MIME type of this object. + * @throws IllegalWriteException if the underlying implementation + * does not support modification of existing values + * @throws IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @throws MessagingException for other failures + */ + void setContent(Object obj, String type) + throws MessagingException; + + /** + * A convenience method that sets the given String as this + * part's content with a MIME type of "text/plain". + * + * @param text The text that is the Message's content. + * @throws IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @throws IllegalStateException if this Part is obtained + * from a READ_ONLY folder + * @throws MessagingException for other failures + */ + void setText(String text) throws MessagingException; + + /** + * Output a bytestream for this Part. This bytestream is + * typically an aggregration of the Part attributes and + * an appropriately encoded bytestream from its 'content'.

+ *

+ * Classes that implement the Part interface decide on + * the appropriate encoding algorithm to be used.

+ *

+ * The bytestream is typically used for sending. + * + * @param os the stream to write to + * @throws IOException if an error occurs writing to the + * stream or if an error is generated + * by the jakarta.activation layer. + * @throws MessagingException if an error occurs fetching the + * data to be written + * @see jakarta.activation.DataHandler#writeTo + */ + void writeTo(OutputStream os) throws IOException, MessagingException; + + /** + * Get all the headers for this header name. Returns null + * if no headers for this header name are available. + * + * @param header_name the name of this header + * @return the value fields for all headers with + * this name + * @throws MessagingException for failures + */ + String[] getHeader(String header_name) + throws MessagingException; + + /** + * Set the value for this header_name. Replaces all existing + * header values with this new value. + * + * @param header_name the name of this header + * @param header_value the value for this header + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @throws MessagingException for other failures + */ + void setHeader(String header_name, String header_value) + throws MessagingException; + + /** + * Add this value to the existing values for this header_name. + * + * @param header_name the name of this header + * @param header_value the value for this header + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @throws MessagingException for other failures + */ + void addHeader(String header_name, String header_value) + throws MessagingException; + + /** + * Remove all headers with this name. + * + * @param header_name the name of this header + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @throws MessagingException for other failures + */ + void removeHeader(String header_name) + throws MessagingException; + + /** + * Return all the headers from this part as an Enumeration of + * Header objects. + * + * @return enumeration of Header objects + * @throws MessagingException for failures + */ + Enumeration

getAllHeaders() throws MessagingException; + + /** + * Return matching headers from this part as an Enumeration of + * Header objects. + * + * @param header_names the headers to match + * @return enumeration of Header objects + * @throws MessagingException for failures + */ + Enumeration
getMatchingHeaders(String[] header_names) + throws MessagingException; + + /** + * Return non-matching headers from this envelope as an Enumeration + * of Header objects. + * + * @param header_names the headers to not match + * @return enumeration of Header objects + * @throws MessagingException for failures + */ + Enumeration
getNonMatchingHeaders(String[] header_names) + throws MessagingException; +} diff --git a/net-mail/src/main/java/jakarta/mail/PasswordAuthentication.java b/net-mail/src/main/java/jakarta/mail/PasswordAuthentication.java new file mode 100644 index 0000000..e354e63 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/PasswordAuthentication.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + + +/** + * The class PasswordAuthentication is a data holder that is used by + * Authenticator. It is simply a repository for a user name and a password. + * + * @author Bill Foote + * @see java.net.PasswordAuthentication + * @see Authenticator + * @see Authenticator#getPasswordAuthentication() + */ + +public final class PasswordAuthentication { + + private final String userName; + private final String password; + + /** + * Initialize a new PasswordAuthentication + * + * @param userName the user name + * @param password The user's password + */ + public PasswordAuthentication(String userName, String password) { + this.userName = userName; + this.password = password; + } + + /** + * @return the user name + */ + public String getUserName() { + return userName; + } + + /** + * @return the password + */ + public String getPassword() { + return password; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/Provider.java b/net-mail/src/main/java/jakarta/mail/Provider.java new file mode 100644 index 0000000..2b58856 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Provider.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * The Provider is a class that describes a protocol + * implementation. The values typically come from the + * javamail.providers and javamail.default.providers + * resource files. An application may also create and + * register a Provider object to dynamically add support + * for a new provider. + * + * @author Max Spivak + * @author Bill Shannon + */ +public class Provider { + + private Type type; + private String protocol, className, vendor, version; + /** + * Create a new provider of the specified type for the specified + * protocol. The specified class implements the provider. + * + * @param type Type.STORE or Type.TRANSPORT + * @param protocol valid protocol for the type + * @param classname class name that implements this protocol + * @param vendor optional string identifying the vendor (may be null) + * @param version optional implementation version string (may be null) + * @since JavaMail 1.4 + */ + public Provider(Type type, String protocol, String classname, + String vendor, String version) { + this.type = type; + this.protocol = protocol; + this.className = classname; + this.vendor = vendor; + this.version = version; + } + + /** + * Returns the type of this Provider. + * + * @return the provider type + */ + public Type getType() { + return type; + } + + /** + * Returns the protocol supported by this Provider. + * + * @return the protocol + */ + public String getProtocol() { + return protocol; + } + + /** + * Returns the name of the class that implements the protocol. + * + * @return the class name + */ + public String getClassName() { + return className; + } + + /** + * Returns the name of the vendor associated with this implementation + * or null. + * + * @return the vendor + */ + public String getVendor() { + return vendor; + } + + /** + * Returns the version of this implementation or null if no version. + * + * @return the version + */ + public String getVersion() { + return version; + } + + /** + * Overrides Object.toString() + */ + @Override + public String toString() { + String s = "jakarta.mail.Provider[" + type + "," + + protocol + "," + className; + + if (vendor != null) + s += "," + vendor; + + if (version != null) + s += "," + version; + + s += "]"; + return s; + } + + /** + * This inner class defines the Provider type. + * Currently, STORE and TRANSPORT are the only two provider types + * supported. + */ + + public static class Type { + /** + * The Provider of type {@code STORE}. + */ + public static final Type STORE = new Type("STORE"); + /** + * The Provider of type {@code TRANSPORT}. + */ + public static final Type TRANSPORT = new Type("TRANSPORT"); + + private String type; + + private Type(String type) { + this.type = type; + } + + @Override + public String toString() { + return type; + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/Quota.java b/net-mail/src/main/java/jakarta/mail/Quota.java new file mode 100644 index 0000000..49f482e --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Quota.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * This class represents a set of quotas for a given quota root. + * Each quota root has a set of resources, represented by the + * Quota.Resource class. Each resource has a name + * (for example, "STORAGE"), a current usage, and a usage limit. + * See RFC 2087. + * + * @author Bill Shannon + * @since JavaMail 1.4 + */ + +public class Quota { + + /** + * The name of the quota root. + */ + public String quotaRoot; + /** + * The set of resources associated with this quota root. + */ + public Resource[] resources; + + /** + * Create a Quota object for the named quotaroot with no associated + * resources. + * + * @param quotaRoot the name of the quota root + */ + public Quota(String quotaRoot) { + this.quotaRoot = quotaRoot; + } + + /** + * Set a resource limit for this quota root. + * + * @param name the name of the resource + * @param limit the resource limit + */ + public void setResourceLimit(String name, long limit) { + if (resources == null) { + resources = new Resource[1]; + resources[0] = new Resource(name, 0, limit); + return; + } + for (int i = 0; i < resources.length; i++) { + if (resources[i].name.equalsIgnoreCase(name)) { + resources[i].limit = limit; + return; + } + } + Resource[] ra = new Resource[resources.length + 1]; + System.arraycopy(resources, 0, ra, 0, resources.length); + ra[ra.length - 1] = new Resource(name, 0, limit); + resources = ra; + } + + /** + * An individual resource in a quota root. + * + * @since JavaMail 1.4 + */ + public static class Resource { + /** + * The name of the resource. + */ + public String name; + /** + * The current usage of the resource. + */ + public long usage; + /** + * The usage limit for the resource. + */ + public long limit; + + /** + * Construct a Resource object with the given name, + * usage, and limit. + * + * @param name the resource name + * @param usage the current usage of the resource + * @param limit the usage limit for the resource + */ + public Resource(String name, long usage, long limit) { + this.name = name; + this.usage = usage; + this.limit = limit; + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/QuotaAwareStore.java b/net-mail/src/main/java/jakarta/mail/QuotaAwareStore.java new file mode 100644 index 0000000..cc188c0 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/QuotaAwareStore.java @@ -0,0 +1,57 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * An interface implemented by Stores that support quotas. + * The {@link #getQuota getQuota} and {@link #setQuota setQuota} methods + * support the quota model defined by the IMAP QUOTA extension. + * Refer to RFC 2087 + * for more information. + * + * @since JavaMail 1.4 + */ +public interface QuotaAwareStore { + /** + * Get the quotas for the named folder. + * Quotas are controlled on the basis of a quota root, not + * (necessarily) a folder. The relationship between folders + * and quota roots depends on the 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 folder the name of the folder + * @return array of Quota objects + * @throws MessagingException if the server doesn't support the + * QUOTA extension + */ + Quota[] getQuota(String folder) throws MessagingException; + + /** + * 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 + * @throws MessagingException if the server doesn't support the + * QUOTA extension + */ + void setQuota(Quota quota) throws MessagingException; +} diff --git a/net-mail/src/main/java/jakarta/mail/ReadOnlyFolderException.java b/net-mail/src/main/java/jakarta/mail/ReadOnlyFolderException.java new file mode 100644 index 0000000..0821d67 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/ReadOnlyFolderException.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * This exception is thrown when an attempt is made to open a folder + * read-write access when the folder is marked read-only.

+ *

+ * The getMessage() method returns more detailed information about the + * error that caused this exception. + * + * @author Jim Glennon + */ +@SuppressWarnings("serial") +public class ReadOnlyFolderException extends MessagingException { + transient private Folder folder; + + /** + * Constructs a ReadOnlyFolderException with the specified + * folder and no detail message. + * + * @param folder the Folder + * @since JavaMail 1.2 + */ + public ReadOnlyFolderException(Folder folder) { + this(folder, null); + } + + /** + * Constructs a ReadOnlyFolderException with the specified + * detail message. + * + * @param folder The Folder + * @param message The detailed error message + * @since JavaMail 1.2 + */ + public ReadOnlyFolderException(Folder folder, String message) { + super(message); + this.folder = folder; + } + + /** + * Constructs a ReadOnlyFolderException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param folder The Folder + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public ReadOnlyFolderException(Folder folder, String message, Exception e) { + super(message, e); + this.folder = folder; + } + + /** + * Returns the Folder object. + * + * @return the Folder + * @since JavaMail 1.2 + */ + public Folder getFolder() { + return folder; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/SendFailedException.java b/net-mail/src/main/java/jakarta/mail/SendFailedException.java new file mode 100644 index 0000000..f6dc9d3 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/SendFailedException.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * This exception is thrown when the message cannot be sent.

+ *

+ * The exception includes those addresses to which the message could not be + * sent as well as the valid addresses to which the message was sent and + * valid addresses to which the message was not sent. + * + * @author John Mani + * @author Max Spivak + * @see Transport#send + * @see Transport#sendMessage + * @see jakarta.mail.event.TransportEvent + */ +@SuppressWarnings("serial") +public class SendFailedException extends MessagingException { + + /** + * The invalid addresses. + */ + transient protected Address[] invalid; + /** + * Valid addresses to which message was sent. + */ + transient protected Address[] validSent; + /** + * Valid addresses to which message was not sent. + */ + transient protected Address[] validUnsent; + + /** + * Constructs a SendFailedException with no detail message. + */ + public SendFailedException() { + super(); + } + + /** + * Constructs a SendFailedException with the specified detail message. + * + * @param s the detail message + */ + public SendFailedException(String s) { + super(s); + } + + /** + * Constructs a SendFailedException with the specified + * Exception and detail message. The specified exception is chained + * to this exception. + * + * @param s the detail message + * @param e the embedded exception + * @see #getNextException + * @see #setNextException + */ + public SendFailedException(String s, Exception e) { + super(s, e); + } + + + /** + * Constructs a SendFailedException with the specified string + * and the specified address objects. + * + * @param msg the detail message + * @param ex the embedded exception + * @param validSent valid addresses to which message was sent + * @param validUnsent valid addresses to which message was not sent + * @param invalid the invalid addresses + * @see #getNextException + * @see #setNextException + */ + public SendFailedException(String msg, Exception ex, Address[] validSent, + Address[] validUnsent, Address[] invalid) { + super(msg, ex); + this.validSent = validSent; + this.validUnsent = validUnsent; + this.invalid = invalid; + } + + /** + * Return the addresses to which this message was sent succesfully. + * + * @return Addresses to which the message was sent successfully or null + */ + public Address[] getValidSentAddresses() { + return validSent; + } + + /** + * Return the addresses that are valid but to which this message + * was not sent. + * + * @return Addresses that are valid but to which the message was + * not sent successfully or null + */ + public Address[] getValidUnsentAddresses() { + return validUnsent; + } + + /** + * Return the addresses to which this message could not be sent. + * + * @return Addresses to which the message sending failed or null; + */ + public Address[] getInvalidAddresses() { + return invalid; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/Service.java b/net-mail/src/main/java/jakarta/mail/Service.java new file mode 100644 index 0000000..0cce025 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Service.java @@ -0,0 +1,632 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import jakarta.mail.event.ConnectionEvent; +import jakarta.mail.event.ConnectionListener; +import jakarta.mail.event.MailEvent; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.EventListener; +import java.util.Vector; +import java.util.concurrent.Executor; + +/** + * An abstract class that contains the functionality + * common to messaging services, such as stores and transports.

+ * A messaging service is created from a Session and is + * named using a URLName. A service must be connected + * before it can be used. Connection events are sent to reflect + * its connection status. + * + * @author Christopher Cotton + * @author Bill Shannon + * @author Kanwar Oberoi + */ + +public abstract class Service implements AutoCloseable { + + /* + * connectionListeners is a Vector, initialized here, + * because we depend on it always existing and depend + * on the synchronization that Vector provides. + * (Sychronizing on the Service object itself can cause + * deadlocks when notifying listeners.) + */ + private final Vector connectionListeners + = new Vector<>(); + /** + * The queue of events to be delivered. + */ + private final EventQueue q; + /** + * The session from which this service was created. + */ + protected Session session; + /** + * The URLName of this service. + */ + protected volatile URLName url = null; + /** + * Debug flag for this service. Set from the session's debug + * flag when this service is created. + */ + protected boolean debug; + private boolean connected = false; + + /** + * Constructor. + * + * @param session Session object for this service + * @param urlname URLName object to be used for this service + */ + protected Service(Session session, URLName urlname) { + this.session = session; + debug = session.getDebug(); + url = urlname; + + /* + * Initialize the URLName with default values. + * The URLName will be updated when connect is called. + */ + String protocol = null; + String host = null; + int port = -1; + String user = null; + String password = null; + String file = null; + + // get whatever information we can from the URL + // XXX - url should always be non-null here, Session + // passes it into the constructor + if (url != null) { + protocol = url.getProtocol(); + host = url.getHost(); + port = url.getPort(); + user = url.getUsername(); + password = url.getPassword(); + file = url.getFile(); + } + + // try to get protocol-specific default properties + if (protocol != null) { + if (host == null) + host = session.getProperty("mail." + protocol + ".host"); + if (user == null) + user = session.getProperty("mail." + protocol + ".user"); + } + + // try to get mail-wide default properties + if (host == null) + host = session.getProperty("mail.host"); + + if (user == null) + user = session.getProperty("mail.user"); + + // try using the system username + if (user == null) { + user = System.getProperty("user.name"); + } + + url = new URLName(protocol, host, port, file, user, password); + + // create or choose the appropriate event queue + String scope = + session.getProperties().getProperty("mail.event.scope", "folder"); + Executor executor = + (Executor) session.getProperties().get("mail.event.executor"); + if (scope.equalsIgnoreCase("application")) + q = EventQueue.getApplicationEventQueue(executor); + else if (scope.equalsIgnoreCase("session")) + q = session.getEventQueue(); + else // if (scope.equalsIgnoreCase("store") || + // scope.equalsIgnoreCase("folder")) + q = new EventQueue(executor); + } + + /** + * A generic connect method that takes no parameters. Subclasses + * can implement the appropriate authentication schemes. Subclasses + * that need additional information might want to use some properties + * or might get it interactively using a popup window.

+ *

+ * If the connection is successful, an "open" ConnectionEvent + * is delivered to any ConnectionListeners on this service.

+ *

+ * Most clients should just call this method to connect to the service.

+ *

+ * It is an error to connect to an already connected service.

+ *

+ * The implementation provided here simply calls the following + * connect(String, String, String) method with nulls. + * + * @throws AuthenticationFailedException for authentication failures + * @throws IllegalStateException if the service is already connected + * @throws MessagingException for other failures + * @see ConnectionEvent + */ + public void connect() throws MessagingException { + connect(null, null, null); + } + + /** + * Connect to the specified address. This method provides a simple + * authentication scheme that requires a username and password.

+ *

+ * If the connection is successful, an "open" ConnectionEvent + * is delivered to any ConnectionListeners on this service.

+ *

+ * It is an error to connect to an already connected service.

+ *

+ * The implementation in the Service class will collect defaults + * for the host, user, and password from the session, from the + * URLName for this service, and from the supplied + * parameters and then call the protocolConnect method. + * If the protocolConnect method returns false, + * the user will be prompted for any missing information and the + * protocolConnect method will be called again. The + * subclass should override the protocolConnect method. + * The subclass should also implement the getURLName + * method, or use the implementation in this class.

+ *

+ * On a successful connection, the setURLName method is + * called with a URLName that includes the information used to make + * the connection, including the password.

+ *

+ * If the username passed in is null, a default value will be chosen + * as described above. + *

+ * If the password passed in is null and this is the first successful + * connection to this service, the user name and the password + * collected from the user will be saved as defaults for subsequent + * connection attempts to this same service when using other Service object + * instances (the connection information is typically always saved within + * a particular Service object instance). The password is saved using the + * Session method setPasswordAuthentication. If the + * password passed in is not null, it is not saved, on the assumption + * that the application is managing passwords explicitly. + * + * @param host the host to connect to + * @param user the user name + * @param password this user's password + * @throws AuthenticationFailedException for authentication failures + * @throws IllegalStateException if the service is already connected + * @throws MessagingException for other failures + * @see ConnectionEvent + * @see Session#setPasswordAuthentication + */ + public void connect(String host, String user, String password) + throws MessagingException { + connect(host, -1, user, password); + } + + /** + * Connect to the current host using the specified username + * and password. This method is equivalent to calling the + * connect(host, user, password) method with null + * for the host name. + * + * @param user the user name + * @param password this user's password + * @throws AuthenticationFailedException for authentication failures + * @throws IllegalStateException if the service is already connected + * @throws MessagingException for other failures + * @see ConnectionEvent + * @see Session#setPasswordAuthentication + * @see #connect(String, String, String) + * @since JavaMail 1.4 + */ + public void connect(String user, String password) + throws MessagingException { + connect(null, user, password); + } + + /** + * Similar to connect(host, user, password) except a specific port + * can be specified. + * + * @param host the host to connect to + * @param port the port to connect to (-1 means the default port) + * @param user the user name + * @param password this user's password + * @throws AuthenticationFailedException for authentication failures + * @throws IllegalStateException if the service is already connected + * @throws MessagingException for other failures + * @see #connect(String, String, String) + * @see ConnectionEvent + */ + public synchronized void connect(String host, int port, + String user, String password) throws MessagingException { + + // see if the service is already connected + if (isConnected()) + throw new IllegalStateException("already connected"); + + PasswordAuthentication pw; + boolean connected = false; + boolean save = false; + String protocol = null; + String file = null; + + // get whatever information we can from the URL + // XXX - url should always be non-null here, Session + // passes it into the constructor + if (url != null) { + protocol = url.getProtocol(); + if (host == null) + host = url.getHost(); + if (port == -1) + port = url.getPort(); + + if (user == null) { + user = url.getUsername(); + if (password == null) // get password too if we need it + password = url.getPassword(); + } else { + if (password == null && user.equals(url.getUsername())) + // only get the password if it matches the username + password = url.getPassword(); + } + + file = url.getFile(); + } + + // try to get protocol-specific default properties + if (protocol != null) { + if (host == null) + host = session.getProperty("mail." + protocol + ".host"); + if (user == null) + user = session.getProperty("mail." + protocol + ".user"); + } + + // try to get mail-wide default properties + if (host == null) + host = session.getProperty("mail.host"); + + if (user == null) + user = session.getProperty("mail.user"); + + // try using the system username + if (user == null) { + user = System.getProperty("user.name"); + } + + // if we don't have a password, look for saved authentication info + if (password == null && url != null) { + // canonicalize the URLName + setURLName(new URLName(protocol, host, port, file, user, null)); + pw = session.getPasswordAuthentication(getURLName()); + if (pw != null) { + if (user == null) { + user = pw.getUserName(); + password = pw.getPassword(); + } else if (user.equals(pw.getUserName())) { + password = pw.getPassword(); + } + } else + save = true; + } + + // try connecting, if the protocol needs some missing + // information (user, password) it will not connect. + // if it tries to connect and fails, remember why for later. + AuthenticationFailedException authEx = null; + try { + connected = protocolConnect(host, port, user, password); + } catch (AuthenticationFailedException ex) { + authEx = ex; + } + + // if not connected, ask the user and try again + if (!connected) { + InetAddress addr; + try { + addr = InetAddress.getByName(host); + } catch (UnknownHostException e) { + addr = null; + } + pw = session.requestPasswordAuthentication( + addr, port, + protocol, + null, user); + if (pw != null) { + user = pw.getUserName(); + password = pw.getPassword(); + + // have the service connect again + connected = protocolConnect(host, port, user, password); + } + } + + // if we're not connected by now, we give up + if (!connected) { + if (authEx != null) + throw authEx; + else if (user == null) + throw new AuthenticationFailedException( + "failed to connect, no user name specified?"); + else if (password == null) + throw new AuthenticationFailedException( + "failed to connect, no password specified?"); + else + throw new AuthenticationFailedException("failed to connect"); + } + + setURLName(new URLName(protocol, host, port, file, user, password)); + + if (save) + session.setPasswordAuthentication(getURLName(), + new PasswordAuthentication(user, password)); + + // set our connected state + setConnected(true); + + // finally, deliver the connection event + notifyConnectionListeners(ConnectionEvent.OPENED); + } + + + /** + * The service implementation should override this method to + * perform the actual protocol-specific connection attempt. + * The default implementation of the connect method + * calls this method as needed.

+ *

+ * The protocolConnect method should return + * false if a user name or password is required + * for authentication but the corresponding parameter is null; + * the connect method will prompt the user when + * needed to supply missing information. This method may + * also return false if authentication fails for + * the supplied user name or password. Alternatively, this method + * may throw an AuthenticationFailedException when authentication + * fails. This exception may include a String message with more + * detail about the failure.

+ *

+ * The protocolConnect method should throw an + * exception to report failures not related to authentication, + * such as an invalid host name or port number, loss of a + * connection during the authentication process, unavailability + * of the server, etc. + * + * @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 + * @throws AuthenticationFailedException for authentication failures + * @throws MessagingException for non-authentication failures + */ + protected boolean protocolConnect(String host, int port, String user, + String password) throws MessagingException { + return false; + } + + /** + * Is this service currently connected?

+ *

+ * This implementation uses a private boolean field to + * store the connection state. This method returns the value + * of that field.

+ *

+ * Subclasses may want to override this method to verify that any + * connection to the message store is still alive. + * + * @return true if the service is connected, false if it is not connected + */ + public synchronized boolean isConnected() { + return connected; + } + + /** + * Set the connection state of this service. The connection state + * will automatically be set by the service implementation during the + * connect and close methods. + * Subclasses will need to call this method to set the state + * if the service was automatically disconnected.

+ *

+ * The implementation in this class merely sets the private field + * returned by the isConnected method. + * + * @param connected true if the service is connected, + * false if it is not connected + */ + protected synchronized void setConnected(boolean connected) { + this.connected = connected; + } + + /** + * Close this service and terminate its connection. A close + * ConnectionEvent is delivered to any ConnectionListeners. Any + * Messaging components (Folders, Messages, etc.) belonging to this + * service are invalid after this service is closed. Note that the service + * is closed even if this method terminates abnormally by throwing + * a MessagingException.

+ *

+ * This implementation uses setConnected(false) to set + * this service's connected state to false. It will then + * send a close ConnectionEvent to any registered ConnectionListeners. + * Subclasses overriding this method to do implementation specific + * cleanup should call this method as a last step to insure event + * notification, probably by including a call to super.close() + * in a finally clause. + * + * @throws MessagingException for errors while closing + * @see ConnectionEvent + */ + public synchronized void close() throws MessagingException { + setConnected(false); + notifyConnectionListeners(ConnectionEvent.CLOSED); + } + + /** + * Return a URLName representing this service. The returned URLName + * does not include the password field.

+ *

+ * Subclasses should only override this method if their + * URLName does not follow the standard format.

+ *

+ * The implementation in the Service class returns (usually a copy of) + * the url field with the password and file information + * stripped out. + * + * @return the URLName representing this service + * @see URLName + */ + public URLName getURLName() { + URLName url = this.url; // snapshot + if (url != null && (url.getPassword() != null || url.getFile() != null)) + return new URLName(url.getProtocol(), url.getHost(), + url.getPort(), null /* no file */, + url.getUsername(), null /* no password */); + else + return url; + } + + /** + * Set the URLName representing this service. + * Normally used to update the url field + * after a service has successfully connected.

+ *

+ * Subclasses should only override this method if their + * URL does not follow the standard format. In particular, + * subclasses should override this method if their URL + * does not require all the possible fields supported by + * URLName; a new URLName should + * be constructed with any unneeded fields removed.

+ *

+ * The implementation in the Service class simply sets the + * url field. + * + * @param url the URLName + * @see URLName + */ + protected void setURLName(URLName url) { + this.url = url; + } + + /** + * Add a listener for Connection events on this service.

+ *

+ * The default implementation provided here adds this listener + * to an internal list of ConnectionListeners. + * + * @param l the Listener for Connection events + * @see ConnectionEvent + */ + public void addConnectionListener(ConnectionListener l) { + connectionListeners.addElement(l); + } + + /** + * Remove a Connection event listener.

+ *

+ * The default implementation provided here removes this listener + * from the internal list of ConnectionListeners. + * + * @param l the listener + * @see #addConnectionListener + */ + public void removeConnectionListener(ConnectionListener l) { + connectionListeners.removeElement(l); + } + + /** + * Notify all ConnectionListeners. Service implementations are + * expected to use this method to broadcast connection events.

+ *

+ * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * ConnectionListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the ConnectionEvent type + */ + protected void notifyConnectionListeners(int type) { + /* + * Don't bother queuing an event if there's no listeners. + * Yes, listeners could be removed after checking, which + * just makes this an expensive no-op. + */ + if (connectionListeners.size() > 0) { + ConnectionEvent e = new ConnectionEvent(this, type); + queueEvent(e, connectionListeners); + } + + /* Fix for broken JDK1.1.x Garbage collector : + * The 'conservative' GC in JDK1.1.x occasionally fails to + * garbage-collect Threads which are in the wait state. + * This would result in thread (and consequently memory) leaks. + * + * We attempt to fix this by sending a 'terminator' event + * to the queue, after we've sent the CLOSED event. The + * terminator event causes the event-dispatching thread to + * self destruct. + */ + if (type == ConnectionEvent.CLOSED) + q.terminateQueue(); + } + + /** + * Return getURLName.toString() if this service has a URLName, + * otherwise it will return the default toString. + */ + @Override + public String toString() { + URLName url = getURLName(); + if (url != null) + return url.toString(); + else + return super.toString(); + } + + /** + * Add the event and vector of listeners to the queue to be delivered. + * + * @param event the event + * @param vector the vector of listeners + */ + protected void queueEvent(MailEvent event, + Vector vector) { + /* + * Copy the vector in order to freeze the state of the set + * of EventListeners the event should be delivered to prior + * to delivery. This ensures that any changes made to the + * Vector from a target listener's method during the delivery + * of this event will not take effect until after the event is + * delivered. + */ + @SuppressWarnings("unchecked") + Vector v = (Vector) vector.clone(); + q.enqueue(event, v); + } + + /** + * Package private method to allow Folder to get the Session for a Store. + */ + Session getSession() { + return session; + } + + /** + * Package private method to allow Folder to get the EventQueue for a Store. + */ + EventQueue getEventQueue() { + return q; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/Session.java b/net-mail/src/main/java/jakarta/mail/Session.java new file mode 100644 index 0000000..4d17b11 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Session.java @@ -0,0 +1,1286 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import jakarta.mail.util.LineInputStream; +import jakarta.mail.util.StreamProvider; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Constructor; +import java.net.InetAddress; +import java.net.URL; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.ServiceLoader; +import java.util.StringTokenizer; +import java.util.concurrent.Executor; +import java.util.logging.Level; + +/** + * Support interface to generalize + * code that loads resources from stream. + */ +interface StreamLoader { + void load(InputStream is) throws IOException; +} + +/** + * The Session class represents a mail session and is not subclassed. + * It collects together properties and defaults used by the mail API's. + * A single default session can be shared by multiple applications on the + * desktop. Unshared sessions can also be created.

+ *

+ * The Session class provides access to the protocol providers that + * implement the Store, Transport, and related + * classes. The protocol providers are configured using the following files: + *

    + *
  • javamail.providers and + * javamail.default.providers
  • + *
  • javamail.address.map and + * javamail.default.address.map
  • + *
+ *

+ * Each javamail.X resource file is searched for using + * three methods in the following order: + *

    + *
  1. java.home/conf/javamail.X
  2. + *
  3. META-INF/javamail.X
  4. + *
  5. META-INF/javamail.default.X
  6. + *
+ *

+ * (Where java.home is the value of the "java.home" System property + * and conf is the directory named "conf" if it exists, + * otherwise the directory named "lib"; the "conf" directory was + * introduced in JDK 1.9.) + *

+ * The first method allows the user to include their own version of the + * resource file by placing it in the conf directory where the + * java.home property points. The second method allows an + * application that uses the Jakarta Mail APIs to include their own resource + * files in their application's or jar file's META-INF + * directory. The javamail.default.X default files + * are part of the Jakarta Mail mail.jar file and should not be + * supplied by users.

+ *

+ * File location depends upon how the ClassLoader method + * getResource is implemented. Usually, the + * getResource method searches through CLASSPATH until it + * finds the requested file and then stops.

+ *

+ * The ordering of entries in the resource files matters. If multiple + * entries exist, the first entries take precedence over the later + * entries. For example, the first IMAP provider found will be set as the + * default IMAP implementation until explicitly changed by the + * application. The user- or system-supplied resource files augment, they + * do not override, the default files included with the Jakarta Mail APIs. + * This means that all entries in all files loaded will be available.

+ * + * javamail.providers and + * javamail.default.providers

+ *

+ * These resource files specify the stores and transports that are + * available on the system, allowing an application to "discover" what + * store and transport implementations are available. The protocol + * implementations are listed one per line. The file format defines four + * attributes that describe a protocol implementation. Each attribute is + * an "="-separated name-value pair with the name in lowercase. Each + * name-value pair is semi-colon (";") separated. The following names + * are defined. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
+ * Attribute Names in Providers Files + *
NameDescription
protocolName assigned to protocol. + * For example, smtp for Transport.
typeValid entries are store and transport.
classClass name that implements this protocol.
vendorOptional string identifying the vendor.
versionOptional string identifying the version.

+ *

+ * Here's an example of META-INF/javamail.default.providers + * file contents: + *

+ * protocol=imap; type=store; class=com.sun.mail.imap.IMAPStore; vendor=Oracle;
+ * protocol=smtp; type=transport; class=com.sun.mail.smtp.SMTPTransport; vendor=Oracle;
+ * 

+ *

+ * The current implementation also supports configuring providers using + * the Java SE {@link ServiceLoader ServiceLoader} mechanism. + * When creating your own provider, create a {@link Provider} subclass, + * for example: + *

+ * package com.example;
+ *
+ * import jakarta.mail.Provider;
+ *
+ * public class MyProvider extends Provider {
+ *     public MyProvider() {
+ *         super(Provider.Type.STORE, "myprot", MyStore.class.getName(),
+ *             "Example", null);
+ *     }
+ * }
+ * 
+ * Then include a file named META-INF/services/jakarta.mail.Provider + * in your jar file that lists the name of your Provider class: + *
+ * com.example.MyProvider
+ * 
+ *

+ * + * javamail.address.map and + * javamail.default.address.map

+ *

+ * These resource files map transport address types to the transport + * protocol. The getType method of + * jakarta.mail.Address returns the address type. The + * javamail.address.map file maps the transport type to the + * protocol. The file format is a series of name-value pairs. Each key + * name should correspond to an address type that is currently installed + * on the system; there should also be an entry for each + * jakarta.mail.Address implementation that is present if it is + * to be used. For example, the + * jakarta.mail.internet.InternetAddress method + * getType returns "rfc822". Each referenced protocol should + * be installed on the system. For the case of news, below, + * the client should install a Transport provider supporting the nntp + * protocol.

+ *

+ * Here are the typical contents of a javamail.address.map file: + *

+ * rfc822=smtp
+ * news=nntp
+ * 
+ * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + */ + +public final class Session { + + // Support legacy @DefaultProvider + private static final String DEFAULT_PROVIDER = "org.xbib.net.mail.util.DefaultProvider"; + private static final String confDir; + // The default session. + private static Session defaultSession = null; + + static { + String dir = null; + String home = System.getProperty("java.home"); + String newdir = home + File.separator + "conf"; + File conf = new File(newdir); + if (conf.exists()) + dir = newdir + File.separator; + else + dir = home + File.separator + + "lib" + File.separator; + confDir = dir; + } + + private final StreamProvider streamProvider; + private final Properties props; + private final Authenticator authenticator; + private final Hashtable authTable + = new Hashtable<>(); + private final List providers = new ArrayList<>(); + private final Map providersByProtocol = new HashMap<>(); + private final Map providersByClassName = new HashMap<>(); + private final Properties addressMap = new Properties(); + // maps type to protocol + // the queue of events to be delivered, if mail.event.scope===session + private final EventQueue q; + private boolean debug = false; + private PrintStream out; // debug output stream + private MailLogger logger; + + // Constructor is not public + private Session(Properties props, Authenticator authenticator) { + this.props = props; + this.authenticator = authenticator; + this.streamProvider = StreamProvider.provider(); + + if (Boolean.parseBoolean(props.getProperty("mail.debug"))) + debug = true; + + initLogger(); + + // get the Class associated with the Authenticator + Class cl; + if (authenticator != null) { + cl = authenticator.getClass(); + } else { + // Use implementation class, because that class loader has access to jakarta.mail module and implementation resources + cl = streamProvider.getClass(); + } + // load the resources + loadProviders(cl); + loadAddressMap(cl); + q = new EventQueue((Executor) props.get("mail.event.executor")); + } + + /** + * Get a new Session object. + * + * @param props Properties object that hold relevant properties.
+ * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * Jakarta Mail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @param authenticator Authenticator object used to call back to + * the application when a user name and password is + * needed. + * @return a new Session object + * @see Authenticator + */ + public static Session getInstance(Properties props, Authenticator authenticator) { + return new Session(props, authenticator); + } + + /** + * Get a new Session object. + * + * @param props Properties object that hold relevant properties.
+ * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * Jakarta Mail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @return a new Session object + * @since JavaMail 1.2 + */ + public static Session getInstance(Properties props) { + return new Session(props, null); + } + + /** + * Get the default Session object. If a default has not yet been + * setup, a new Session object is created and installed as the + * default.

+ *

+ * Since the default session is potentially available to all + * code executing in the same Java virtual machine, and the session + * can contain security sensitive information such as user names + * and passwords, access to the default session is restricted. + * The Authenticator object, which must be created by the caller, + * is used indirectly to check access permission. The Authenticator + * object passed in when the session is created is compared with + * the Authenticator object passed in to subsequent requests to + * get the default session. If both objects are the same, or are + * from the same ClassLoader, the request is allowed. Otherwise, + * it is denied.

+ *

+ * Note that if the Authenticator object used to create the session + * is null, anyone can get the default session by passing in null.

+ *

+ * Note also that the Properties object is used only the first time + * this method is called, when a new Session object is created. + * Subsequent calls return the Session object that was created by the + * first call, and ignore the passed Properties object. Use the + * getInstance method to get a new Session object every + * time the method is called.

+ *

+ * Additional security Permission objects may be used to + * control access to the default session.

+ *

+ * In the current implementation, if a SecurityManager is set, the + * caller must have the RuntimePermission("setFactory") + * permission. + * + * @param props Properties object. Used only if a new Session + * object is created.
+ * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * Jakarta Mail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @param authenticator Authenticator object. Used only if a + * new Session object is created. Otherwise, + * it must match the Authenticator used to create + * the Session. + * @return the default Session object + */ + public static synchronized Session getDefaultInstance(Properties props, Authenticator authenticator) { + if (defaultSession == null) { + defaultSession = new Session(props, authenticator); + } else { + // have to check whether caller is allowed to see default session + if (defaultSession.authenticator == authenticator) { + ; // either same object or both null, either way OK + } else if (defaultSession.authenticator != null && + authenticator != null && + defaultSession.authenticator.getClass().getClassLoader() == + authenticator.getClass().getClassLoader()) { + ; // both objects came from the same class loader, OK + } else + // anything else is not allowed + throw new IllegalStateException("Access to default session denied"); + } + + return defaultSession; + } + + /** + * Get the default Session object. If a default has not yet been + * setup, a new Session object is created and installed as the + * default.

+ *

+ * Note that a default session created with no Authenticator is + * available to all code executing in the same Java virtual + * machine, and the session can contain security sensitive + * information such as user names and passwords. + * + * @param props Properties object. Used only if a new Session + * object is created.
+ * It is expected that the client supplies values + * for the properties listed in Appendix A of the + * Jakarta Mail spec (particularly mail.store.protocol, + * mail.transport.protocol, mail.host, mail.user, + * and mail.from) as the defaults are unlikely to + * work in all cases. + * @return the default Session object + * @since JavaMail 1.2 + */ + public static Session getDefaultInstance(Properties props) { + return getDefaultInstance(props, null); + } + + static boolean containsDefaultProvider(Provider provider) { + Annotation[] annotations = provider.getClass().getDeclaredAnnotations(); + for (Annotation annotation : annotations) { + if (DEFAULT_PROVIDER.equals(annotation.annotationType().getName())) { + return true; + } + } + return false; + } + + private static ClassLoader[] getClassLoaders(final Class... classes) { + ClassLoader[] loaders = new ClassLoader[classes.length]; + int w = 0; + for (Class k : classes) { + ClassLoader cl; + if (k == Thread.class) { + cl = Thread.currentThread().getContextClassLoader(); + } else if (k == System.class) { + cl = ClassLoader.getSystemClassLoader(); + } else { + cl = k.getClassLoader(); + } + if (cl != null) { + loaders[w++] = cl; + } + } + if (loaders.length != w) { + loaders = Arrays.copyOf(loaders, w); + } + return loaders; + } + + private static InputStream getResourceAsStream(final Class c, final String name) throws IOException { + try { + return c.getClassLoader().getResourceAsStream(name); + } catch (RuntimeException e) { + throw new IOException("ClassLoader.getResourceAsStream failed"); + } + } + + private static URL[] getResources(final ClassLoader cl, final String name) { + URL[] ret = null; + try { + List v = Collections.list(cl.getResources(name)); + if (!v.isEmpty()) { + ret = new URL[v.size()]; + v.toArray(ret); + } + } catch (IOException ioex) { + } + return ret; + } + + private static URL[] getSystemResources(final String name) { + URL[] ret = null; + try { + List v = Collections.list(ClassLoader.getSystemResources(name)); + if (!v.isEmpty()) { + ret = new URL[v.size()]; + v.toArray(ret); + } + } catch (IOException ioex) { + } + return ret; + } + + private static InputStream openStream(final URL url) throws IOException { + return url.openStream(); + } + + /** + * Get the stream provider instance of the session. + * + * @return the stream provider + * @since JavaMail 2.1 + */ + public StreamProvider getStreamProvider() { + return streamProvider; + } + + private final synchronized void initLogger() { + logger = new MailLogger(this.getClass(), "DEBUG", debug, getDebugOut()); + } + + /** + * Get the debug setting for this Session. + * + * @return current debug setting + */ + public synchronized boolean getDebug() { + return debug; + } + + /** + * Set the debug setting for this Session. + *

+ * Since the debug setting can be turned on only after the Session + * has been created, to turn on debugging in the Session + * constructor, set the property mail.debug in the + * Properties object passed in to the constructor to true. The + * value of the mail.debug property is used to + * initialize the per-Session debugging flag. Subsequent calls to + * the setDebug method manipulate the per-Session + * debugging flag and have no effect on the mail.debug + * property. + * + * @param debug Debug setting + */ + public synchronized void setDebug(boolean debug) { + this.debug = debug; + initLogger(); + } + + /** + * Returns the stream to be used for debugging output. If no stream + * has been set, System.out is returned. + * + * @return the PrintStream to use for debugging output + * @since JavaMail 1.3 + */ + public synchronized PrintStream getDebugOut() { + if (out == null) + return System.out; + else + return out; + } + + /** + * Set the stream to be used for debugging output for this session. + * If out is null, System.out will be used. + * Note that debugging output that occurs before any session is created, + * as a result of setting the mail.debug system property, + * will always be sent to System.out. + * + * @param out the PrintStream to use for debugging output + * @since JavaMail 1.3 + */ + public synchronized void setDebugOut(PrintStream out) { + this.out = out; + initLogger(); + } + + /** + * This method returns an array of all the implementations installed + * via the javamail.[default.]providers files that can + * be loaded using the ClassLoader available to this application. + * + * @return Array of configured providers + */ + public synchronized Provider[] getProviders() { + Provider[] _providers = new Provider[providers.size()]; + providers.toArray(_providers); + return _providers; + } + + /** + * Returns the default Provider for the protocol + * specified. Checks mail.<protocol>.class property + * first and if it exists, returns the Provider + * associated with this implementation. If it doesn't exist, + * returns the Provider that appeared first in the + * configuration files. If an implementation for the protocol + * isn't found, throws NoSuchProviderException + * + * @param protocol Configured protocol (i.e. smtp, imap, etc) + * @return Currently configured Provider for the specified protocol + * @throws NoSuchProviderException If a provider for the given + * protocol is not found. + */ + public synchronized Provider getProvider(String protocol) throws NoSuchProviderException { + + if (protocol == null || protocol.length() == 0) { + throw new NoSuchProviderException("Invalid protocol: null"); + } + + Provider _provider = null; + + // check if the mail..class property exists + String _className = props.getProperty("mail." + protocol + ".class"); + if (_className != null) { + if (logger.isLoggable(Level.FINE)) { + logger.fine("mail." + protocol + + ".class property exists and points to " + + _className); + } + _provider = providersByClassName.get(_className); + } + + if (_provider != null) { + return _provider; + } else { + // returning currently default protocol in providersByProtocol + _provider = providersByProtocol.get(protocol); + } + + if (_provider == null) { + throw new NoSuchProviderException("No provider for " + protocol); + } else { + if (logger.isLoggable(Level.FINE)) { + logger.fine("getProvider() returning " + _provider); + } + return _provider; + } + } + + /** + * Set the passed Provider to be the default implementation + * for the protocol in Provider.protocol overriding any previous values. + * + * @param provider Currently configured Provider which will be + * set as the default for the protocol + * @throws NoSuchProviderException If the provider passed in + * is invalid. + */ + public synchronized void setProvider(Provider provider) + throws NoSuchProviderException { + if (provider == null) { + throw new NoSuchProviderException("Can't set null provider"); + } + providersByProtocol.put(provider.getProtocol(), provider); + providersByClassName.put(provider.getClassName(), provider); + props.put("mail." + provider.getProtocol() + ".class", provider.getClassName()); + } + + /** + * Get a Store object that implements this user's desired Store + * protocol. The mail.store.protocol property specifies the + * desired protocol. If an appropriate Store object is not obtained, + * NoSuchProviderException is thrown + * + * @return a Store object + * @throws NoSuchProviderException If a provider for the given + * protocol is not found. + */ + public Store getStore() throws NoSuchProviderException { + return getStore(getProperty("mail.store.protocol")); + } + + /** + * Get a Store object that implements the specified protocol. If an + * appropriate Store object cannot be obtained, + * NoSuchProviderException is thrown. + * + * @param protocol the Store protocol + * @return a Store object + * @throws NoSuchProviderException If a provider for the given + * protocol is not found. + */ + public Store getStore(String protocol) throws NoSuchProviderException { + return getStore(new URLName(protocol, null, -1, null, null, null)); + } + + /** + * Get a Store object for the given URLName. If the requested Store + * object cannot be obtained, NoSuchProviderException is thrown. + *

+ * The "scheme" part of the URL string (Refer RFC 1738) is used + * to locate the Store protocol. + * + * @param url URLName that represents the desired Store + * @return a closed Store object + * @throws NoSuchProviderException If a provider for the given + * URLName is not found. + * @see #getFolder(URLName) + * @see URLName + */ + public Store getStore(URLName url) throws NoSuchProviderException { + String protocol = url.getProtocol(); + Provider p = getProvider(protocol); + return getStore(p, url); + } + + /** + * Get an instance of the store specified by Provider. Instantiates + * the store and returns it. + * + * @param provider Store Provider that will be instantiated + * @return Instantiated Store + * @throws NoSuchProviderException If a provider for the given + * Provider is not found. + */ + public Store getStore(Provider provider) throws NoSuchProviderException { + return getStore(provider, null); + } + + /** + * Get an instance of the store specified by Provider. If the URLName + * is not null, uses it, otherwise creates a new one. Instantiates + * the store and returns it. This is a private method used by + * getStore(Provider) and getStore(URLName) + * + * @param provider Store Provider that will be instantiated + * @param url URLName used to instantiate the Store + * @return Instantiated Store + * @throws NoSuchProviderException If a provider for the given + * Provider/URLName is not found. + */ + private Store getStore(Provider provider, URLName url) throws NoSuchProviderException { + + // make sure we have the correct type of provider + if (provider == null || provider.getType() != Provider.Type.STORE) { + throw new NoSuchProviderException("invalid provider"); + } + + return getService(provider, url, Store.class); + } + + /** + * Get a closed Folder object for the given URLName. If the requested + * Folder object cannot be obtained, null is returned.

+ *

+ * The "scheme" part of the URL string (Refer RFC 1738) is used + * to locate the Store protocol. The rest of the URL string (that is, + * the "schemepart", as per RFC 1738) is used by that Store + * in a protocol dependent manner to locate and instantiate the + * appropriate Folder object.

+ *

+ * Note that RFC 1738 also specifies the syntax for the + * "schemepart" for IP-based protocols (IMAP4, POP3, etc.). + * Providers of IP-based mail Stores should implement that + * syntax for referring to Folders. + * + * @param url URLName that represents the desired folder + * @return Folder + * @throws NoSuchProviderException If a provider for the given + * URLName is not found. + * @throws MessagingException if the Folder could not be + * located or created. + * @see #getStore(URLName) + * @see URLName + */ + public Folder getFolder(URLName url) throws MessagingException { + // First get the Store + Store store = getStore(url); + store.connect(); + return store.getFolder(url); + } + + /** + * Get a Transport object that implements this user's desired + * Transport protcol. The mail.transport.protocol property + * specifies the desired protocol. If an appropriate Transport + * object cannot be obtained, MessagingException is thrown. + * + * @return a Transport object + * @throws NoSuchProviderException If the provider is not found. + */ + public Transport getTransport() throws NoSuchProviderException { + String prot = getProperty("mail.transport.protocol"); + if (prot != null) + return getTransport(prot); + // if the property isn't set, use the protocol for "rfc822" + prot = (String) addressMap.get("rfc822"); + if (prot != null) + return getTransport(prot); + return getTransport("smtp"); // if all else fails + } + + /** + * Get a Transport object that implements the specified protocol. + * If an appropriate Transport object cannot be obtained, null is + * returned. + * + * @param protocol the Transport protocol + * @return a Transport object + * @throws NoSuchProviderException If provider for the given + * protocol is not found. + */ + public Transport getTransport(String protocol) + throws NoSuchProviderException { + return getTransport(new URLName(protocol, null, -1, null, null, null)); + } + + /** + * Get a Transport object for the given URLName. If the requested + * Transport object cannot be obtained, NoSuchProviderException is thrown. + *

+ * The "scheme" part of the URL string (Refer RFC 1738) is used + * to locate the Transport protocol. + * + * @param url URLName that represents the desired Transport + * @return a closed Transport object + * @throws NoSuchProviderException If a provider for the given + * URLName is not found. + * @see URLName + */ + public Transport getTransport(URLName url) throws NoSuchProviderException { + String protocol = url.getProtocol(); + Provider p = getProvider(protocol); + return getTransport(p, url); + } + + /** + * Get an instance of the transport specified in the Provider. Instantiates + * the transport and returns it. + * + * @param provider Transport Provider that will be instantiated + * @return Instantiated Transport + * @throws NoSuchProviderException If provider for the given + * provider is not found. + */ + public Transport getTransport(Provider provider) throws NoSuchProviderException { + return getTransport(provider, null); + } + + /** + * Get a Transport object that can transport a Message of the + * specified address type. + * + * @param address an address for which a Transport is needed + * @return A Transport object + * @throws NoSuchProviderException If provider for the + * Address type is not found + * @see Address + */ + public Transport getTransport(Address address) throws NoSuchProviderException { + + String transportProtocol; + transportProtocol = + getProperty("mail.transport.protocol." + address.getType()); + if (transportProtocol != null) + return getTransport(transportProtocol); + transportProtocol = (String) addressMap.get(address.getType()); + if (transportProtocol != null) + return getTransport(transportProtocol); + throw new NoSuchProviderException("No provider for Address type: " + address.getType()); + } + + /** + * Get a Transport object using the given provider and urlname. + * + * @param provider the provider to use + * @param url urlname to use (can be null) + * @return A Transport object + * @throws NoSuchProviderException If no provider or the provider + * was the wrong class. + */ + + private Transport getTransport(Provider provider, URLName url) throws NoSuchProviderException { + // make sure we have the correct type of provider + if (provider == null || provider.getType() != Provider.Type.TRANSPORT) { + throw new NoSuchProviderException("invalid provider"); + } + + return getService(provider, url, Transport.class); + } + + /** + * Get a Service object. Needs a provider object, but will + * create a URLName if needed. It attempts to instantiate + * the correct class. + * + * @param provider which provider to use + * @param url which URLName to use (can be null) + * @param type the service type (class) + * @throws NoSuchProviderException thrown when the class cannot be + * found or when it does not have the correct constructor + * (Session, URLName), or if it is not derived from + * Service. + */ + private T getService(Provider provider, URLName url, Class type) throws NoSuchProviderException { + // need a provider and url + if (provider == null) { + throw new NoSuchProviderException("null"); + } + + // create a url if needed + if (url == null) { + url = new URLName(provider.getProtocol(), null, -1, + null, null, null); + } + + // get the ClassLoader associated with the Authenticator + Class acl; + if (authenticator != null) + acl = authenticator.getClass(); + else + acl = streamProvider.getClass(); + + Class serviceClass = null; + for (ClassLoader l : getClassLoaders(Thread.class, + provider.getClass(), + acl, + streamProvider.getClass(), + getClass(), + System.class)) { + try { + //load and verify provider is compatible in this classloader + serviceClass = Class.forName(provider.getClassName(), + false, l).asSubclass(type); + break; + } catch (ClassNotFoundException | ClassCastException ex) { + // ignore it + } + } + + if (serviceClass == null) { + // 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.) + try { + serviceClass = Class.forName(provider.getClassName()) + .asSubclass(type); + } catch (Exception ex) { + // Nothing worked, give up. + logger.log(Level.FINE, "Exception loading provider", ex); + throw new NoSuchProviderException(provider.getProtocol()); + } + } + + // construct an instance of the class + try { + Class[] c = {Session.class, URLName.class}; + Constructor cons = serviceClass.getConstructor(c); + + Object[] o = {this, url}; + return type.cast(cons.newInstance(o)); + } catch (Exception ex) { + logger.log(Level.FINE, "Exception loading provider", ex); + throw new NoSuchProviderException(provider.getProtocol()); + } + } + + /** + * Save a PasswordAuthentication for this (store or transport) URLName. + * If pw is null the entry corresponding to the URLName is removed. + *

+ * This is normally used only by the store or transport implementations + * to allow authentication information to be shared among multiple + * uses of a session. + * + * @param url the URLName + * @param pw the PasswordAuthentication to save + */ + public void setPasswordAuthentication(URLName url, PasswordAuthentication pw) { + if (pw == null) + authTable.remove(url); + else + authTable.put(url, pw); + } + + /** + * Return any saved PasswordAuthentication for this (store or transport) + * URLName. Normally used only by store or transport implementations. + * + * @param url the URLName + * @return the PasswordAuthentication corresponding to the URLName + */ + public PasswordAuthentication getPasswordAuthentication(URLName url) { + return authTable.get(url); + } + + /** + * Call back to the application to get the needed user name and password. + * The application should put up a dialog something like: + *

+     * Connecting to <protocol> mail service on host <addr>, port <port>.
+     * <prompt>
+     *
+     * User Name: <defaultUserName>
+     * Password:
+     * 
+ * + * @param addr InetAddress of the host. may be null. + * @param port the port on the host + * @param protocol protocol scheme (e.g. imap, pop3, etc.) + * @param prompt any additional String to show as part of + * the prompt; may be null. + * @param defaultUserName the default username. may be null. + * @return the authentication which was collected by the authenticator; + * may be null. + */ + public PasswordAuthentication requestPasswordAuthentication(InetAddress addr, int port, String protocol, String prompt, String defaultUserName) { + if (authenticator != null) { + return authenticator.requestPasswordAuthentication( + addr, port, protocol, prompt, defaultUserName); + } else { + return null; + } + } + + /** + * Returns the Properties object associated with this Session + * + * @return Properties object + */ + public Properties getProperties() { + return props; + } + + /** + * Returns the value of the specified property. Returns null + * if this property does not exist. + * + * @param name the property name + * @return String that is the property value + */ + public String getProperty(String name) { + return props.getProperty(name); + } + + /** + * Load the protocol providers config files. + */ + private void loadProviders(Class cl) { + StreamLoader loader = new StreamLoader() { + @Override + public void load(InputStream is) throws IOException { + loadProvidersFromStream(is); + } + }; + + // load system-wide javamail.providers from the + // /{conf,lib} directory + if (confDir != null) + loadFile(confDir + "javamail.providers", loader); + + //Fetch classloader of given class, falling back to others if needed. + ClassLoader gcl; + ClassLoader[] loaders = getClassLoaders(cl, Thread.class, System.class); + if (loaders.length != 0) { + gcl = loaders[0]; + } else { + gcl = getClass().getClassLoader(); //getContextClassLoader(); //Fail safe + } + + // next, add all the non-default services + ServiceLoader sl = ServiceLoader.load(Provider.class, gcl); + for (Provider p : sl) { + if (!containsDefaultProvider(p)) + addProvider(p); + } + + // load the META-INF/javamail.providers file supplied by an application + loadAllResources("META-INF/javamail.providers", cl, loader); + + // load default META-INF/javamail.default.providers from mail.jar file + loadResource("META-INF/javamail.default.providers", cl, loader, false); + + // finally, add all the default services + sl = ServiceLoader.load(Provider.class, gcl); + for (Provider p : sl) { + if (containsDefaultProvider(p)) + addProvider(p); + } + + /* + * If we haven't loaded any providers, fake it. + */ + if (providers.isEmpty()) { + logger.config("failed to load any providers, using defaults"); + // failed to load any providers, initialize with our defaults + addProvider(new Provider(Provider.Type.STORE, + "imap", "org.xbib.net.mail.imap.IMAPStore", + "xbib", "")); + addProvider(new Provider(Provider.Type.STORE, + "imaps", "org.xbib.net.mail.imap.IMAPSSLStore", + "xbib", "")); + addProvider(new Provider(Provider.Type.STORE, + "pop3", "org.xbib.net.mail.pop3.POP3Store", + "xbib", "")); + addProvider(new Provider(Provider.Type.STORE, + "pop3s", "org.xbib.net.mail.pop3.POP3SSLStore", + "xbib", "")); + addProvider(new Provider(Provider.Type.TRANSPORT, + "smtp", "org.xbib.net.mail.smtp.SMTPTransport", + "xbib", "")); + addProvider(new Provider(Provider.Type.TRANSPORT, + "smtps", "org.xbib.net.mail.smtp.SMTPSSLTransport", + "xbib", "")); + } + + if (logger.isLoggable(Level.CONFIG)) { + // dump the output of the tables for debugging + logger.config("Tables of loaded providers"); + logger.config("Providers Listed By Class Name: " + + providersByClassName); + logger.config("Providers Listed By Protocol: " + + providersByProtocol); + } + } + + /* + * Following are security related methods that work on JDK 1.2 or newer. + */ + + private void loadProvidersFromStream(InputStream is) throws IOException { + if (is != null) { + LineInputStream lis = streamProvider.inputLineStream(is, false); + String currLine; + + // load and process one line at a time using LineInputStream + while ((currLine = lis.readLine()) != null) { + + if (currLine.startsWith("#")) + continue; + if (currLine.trim().isEmpty()) + continue; // skip blank line + Provider.Type type = null; + String protocol = null, className = null; + String vendor = null, version = null; + + // separate line into key-value tuples + StringTokenizer tuples = new StringTokenizer(currLine, ";"); + while (tuples.hasMoreTokens()) { + String currTuple = tuples.nextToken().trim(); + + // set the value of each attribute based on its key + int sep = currTuple.indexOf("="); + if (currTuple.startsWith("protocol=")) { + protocol = currTuple.substring(sep + 1); + } else if (currTuple.startsWith("type=")) { + String strType = currTuple.substring(sep + 1); + if (strType.equalsIgnoreCase("store")) { + type = Provider.Type.STORE; + } else if (strType.equalsIgnoreCase("transport")) { + type = Provider.Type.TRANSPORT; + } + } else if (currTuple.startsWith("class=")) { + className = currTuple.substring(sep + 1); + } else if (currTuple.startsWith("vendor=")) { + vendor = currTuple.substring(sep + 1); + } else if (currTuple.startsWith("version=")) { + version = currTuple.substring(sep + 1); + } + } + + // check if a valid Provider; else, continue + if (type == null || protocol == null || className == null + || protocol.length() == 0 || className.length() == 0) { + + logger.log(Level.CONFIG, "Bad provider entry: {0}", + currLine); + continue; + } + Provider provider = new Provider(type, protocol, className, + vendor, version); + + // add the newly-created Provider to the lookup tables + addProvider(provider); + } + } + } + + /** + * Add a provider to the session. + * + * @param provider the provider to add + * @since JavaMail 1.4 + */ + public synchronized void addProvider(Provider provider) { + providers.add(provider); + providersByClassName.put(provider.getClassName(), provider); + if (!providersByProtocol.containsKey(provider.getProtocol())) + providersByProtocol.put(provider.getProtocol(), provider); + } + + // load maps in reverse order of preference so that the preferred + // map is loaded last since its entries will override the previous ones + private void loadAddressMap(Class cl) { + StreamLoader loader = addressMap::load; + + // load default META-INF/javamail.default.address.map from mail.jar + loadResource("META-INF/javamail.default.address.map", cl, loader, true); + + // load the META-INF/javamail.address.map file supplied by an app + loadAllResources("META-INF/javamail.address.map", cl, loader); + + // load system-wide javamail.address.map from the + // /{conf,lib} directory + if (confDir != null) + loadFile(confDir + "javamail.address.map", loader); + + if (addressMap.isEmpty()) { + logger.config("failed to load address map, using defaults"); + addressMap.put("rfc822", "smtp"); + } + } + + /** + * Set the default transport protocol to use for addresses of + * the specified type. Normally the default is set by the + * javamail.default.address.map or + * javamail.address.map files or resources. + * + * @param addresstype type of address + * @param protocol name of protocol + * @see #getTransport(Address) + * @since JavaMail 1.4 + */ + public synchronized void setProtocolForAddress(String addresstype, String protocol) { + if (protocol == null) + addressMap.remove(addresstype); + else + addressMap.put(addresstype, protocol); + } + + /** + * Load from the named file. + */ + private void loadFile(String name, StreamLoader loader) { + InputStream clis = null; + try { + clis = new BufferedInputStream(new FileInputStream(name)); + loader.load(clis); + logger.log(Level.CONFIG, "successfully loaded file: {0}", name); + } catch (FileNotFoundException fex) { + // ignore it + } catch (IOException e) { + if (logger.isLoggable(Level.CONFIG)) + logger.log(Level.CONFIG, "not loading file: " + name, e); + } finally { + try { + if (clis != null) + clis.close(); + } catch (IOException ex) { + } // ignore it + } + } + + /** + * Load from the named resource. + */ + private void loadResource(String name, Class cl, StreamLoader loader, boolean expected) { + try (InputStream clis = getResourceAsStream(cl, name)) { + if (clis != null) { + loader.load(clis); + logger.log(Level.CONFIG, "successfully loaded resource: {0}", + name); + } else { + if (expected) + logger.log(Level.WARNING, + "expected resource not found: {0}", name); + } + } catch (IOException e) { + logger.log(Level.CONFIG, "Exception loading resource", e); + } + // ignore it + } + + /** + * Load all of the named resource. + */ + private void loadAllResources(String name, Class cl, StreamLoader loader) { + boolean anyLoaded = false; + try { + URL[] urls; + ClassLoader cld = cl.getClassLoader(); + if (cld != null) + urls = getResources(cld, name); + else + urls = getSystemResources(name); + if (urls != null) { + for (URL url : urls) { + logger.log(Level.CONFIG, "URL {0}", url); + try (InputStream clis = openStream(url)) { + if (clis != null) { + loader.load(clis); + anyLoaded = true; + logger.log(Level.CONFIG, + "successfully loaded resource: {0}", url); + } else { + logger.log(Level.CONFIG, + "not loading resource: {0}", url); + } + } catch (FileNotFoundException fex) { + // ignore it + } catch (IOException ioex) { + logger.log(Level.CONFIG, "Exception loading resource", + ioex); + } + } + } + } catch (Exception ex) { + logger.log(Level.CONFIG, "Exception loading resource", ex); + } + + // if failed to load anything, fall back to old technique, just in case + if (!anyLoaded) { + /* + logger.config("!anyLoaded"); + */ + loadResource("/" + name, cl, loader, false); + } + } + + EventQueue getEventQueue() { + return q; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/Store.java b/net-mail/src/main/java/jakarta/mail/Store.java new file mode 100644 index 0000000..b2b913a --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Store.java @@ -0,0 +1,300 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import jakarta.mail.event.FolderEvent; +import jakarta.mail.event.FolderListener; +import jakarta.mail.event.StoreEvent; +import jakarta.mail.event.StoreListener; +import java.util.Vector; + +/** + * An abstract class that models a message store and its + * access protocol, for storing and retrieving messages. + * Subclasses provide actual implementations.

+ *

+ * Note that Store extends the Service + * class, which provides many common methods for naming stores, + * connecting to stores, and listening to connection events. + * + * @author John Mani + * @author Bill Shannon + * @see Service + * @see jakarta.mail.event.ConnectionEvent + * @see StoreEvent + */ + +public abstract class Store extends Service { + + // Vector of Store listeners + private volatile Vector storeListeners = null; + // Vector of folder listeners + private volatile Vector folderListeners = null; + + /** + * Constructor. + * + * @param session Session object for this Store. + * @param urlname URLName object to be used for this Store + */ + protected Store(Session session, URLName urlname) { + super(session, urlname); + } + + /** + * Returns a Folder object that represents the 'root' of + * the default namespace presented to the user by the Store. + * + * @return the root Folder + * @throws IllegalStateException if this Store is not connected. + * @throws MessagingException for other failures + */ + public abstract Folder getDefaultFolder() throws MessagingException; + + /** + * Return the Folder object corresponding to the given name. Note + * that a Folder object is returned even if the named folder does + * not physically exist on the Store. The exists() + * method on the folder object indicates whether this folder really + * exists.

+ *

+ * Folder objects are not cached by the Store, so invoking this + * method on the same name multiple times will return that many + * distinct Folder objects. + * + * @param name The name of the Folder. In some Stores, name can + * be an absolute path if it starts with the + * hierarchy delimiter. Else it is interpreted + * relative to the 'root' of this namespace. + * @return Folder object + * @throws IllegalStateException if this Store is not connected. + * @throws MessagingException for other failures + * @see Folder#create + * @see Folder#exists + */ + public abstract Folder getFolder(String name) + throws MessagingException; + + /** + * Return a closed Folder object, corresponding to the given + * URLName. The store specified in the given URLName should + * refer to this Store object.

+ *

+ * Implementations of this method may obtain the name of the + * actual folder using the getFile() method on + * URLName, and use that name to create the folder. + * + * @param url URLName that denotes a folder + * @return Folder object + * @throws IllegalStateException if this Store is not connected. + * @throws MessagingException for other failures + * @see URLName + */ + public abstract Folder getFolder(URLName url) + throws MessagingException; + + /** + * Return a set of folders representing the personal namespaces + * for the current user. A personal namespace is a set of names that + * is considered within the personal scope of the authenticated user. + * Typically, only the authenticated user has access to mail folders + * in their personal namespace. If an INBOX exists for a user, it + * must appear within the user's personal namespace. In the + * typical case, there should be only one personal namespace for each + * user in each Store.

+ *

+ * This implementation returns an array with a single entry containing + * the return value of the getDefaultFolder method. + * Subclasses should override this method to return appropriate information. + * + * @return array of Folder objects + * @throws IllegalStateException if this Store is not connected. + * @throws MessagingException for other failures + * @since JavaMail 1.2 + */ + public Folder[] getPersonalNamespaces() throws MessagingException { + return new Folder[]{getDefaultFolder()}; + } + + /** + * Return a set of folders representing the namespaces for + * user. The namespaces returned represent the + * personal namespaces for the user. To access mail folders in the + * other user's namespace, the currently authenticated user must be + * explicitly granted access rights. For example, it is common for + * a manager to grant to their secretary access rights to their + * mail folders.

+ *

+ * This implementation returns an empty array. Subclasses should + * override this method to return appropriate information. + * + * @param user the user name + * @return array of Folder objects + * @throws IllegalStateException if this Store is not connected. + * @throws MessagingException for other failures + * @since JavaMail 1.2 + */ + public Folder[] getUserNamespaces(String user) + throws MessagingException { + return new Folder[0]; + } + + /** + * Return a set of folders representing the shared namespaces. + * A shared namespace is a namespace that consists of mail folders + * that are intended to be shared amongst users and do not exist + * within a user's personal namespace.

+ *

+ * This implementation returns an empty array. Subclasses should + * override this method to return appropriate information. + * + * @return array of Folder objects + * @throws IllegalStateException if this Store is not connected. + * @throws MessagingException for other failures + * @since JavaMail 1.2 + */ + public Folder[] getSharedNamespaces() throws MessagingException { + return new Folder[0]; + } + + /** + * Add a listener for StoreEvents on this Store.

+ *

+ * The default implementation provided here adds this listener + * to an internal list of StoreListeners. + * + * @param l the Listener for Store events + * @see StoreEvent + */ + public synchronized void addStoreListener(StoreListener l) { + if (storeListeners == null) + storeListeners = new Vector<>(); + storeListeners.addElement(l); + } + + /** + * Remove a listener for Store events.

+ *

+ * The default implementation provided here removes this listener + * from the internal list of StoreListeners. + * + * @param l the listener + * @see #addStoreListener + */ + public synchronized void removeStoreListener(StoreListener l) { + if (storeListeners != null) + storeListeners.removeElement(l); + } + + /** + * Notify all StoreListeners. Store implementations are + * expected to use this method to broadcast StoreEvents.

+ *

+ * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * StoreListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the StoreEvent type + * @param message a message for the StoreEvent + */ + protected void notifyStoreListeners(int type, String message) { + if (storeListeners == null) + return; + + StoreEvent e = new StoreEvent(this, type, message); + queueEvent(e, storeListeners); + } + + /** + * Add a listener for Folder events on any Folder object + * obtained from this Store. FolderEvents are delivered to + * FolderListeners on the affected Folder as well as to + * FolderListeners on the containing Store.

+ *

+ * The default implementation provided here adds this listener + * to an internal list of FolderListeners. + * + * @param l the Listener for Folder events + * @see FolderEvent + */ + public synchronized void addFolderListener(FolderListener l) { + if (folderListeners == null) + folderListeners = new Vector<>(); + folderListeners.addElement(l); + } + + /** + * Remove a listener for Folder events.

+ *

+ * The default implementation provided here removes this listener + * from the internal list of FolderListeners. + * + * @param l the listener + * @see #addFolderListener + */ + public synchronized void removeFolderListener(FolderListener l) { + if (folderListeners != null) + folderListeners.removeElement(l); + } + + /** + * Notify all FolderListeners. Store implementations are + * expected to use this method to broadcast Folder events.

+ *

+ * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * FolderListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type type of FolderEvent + * @param folder affected Folder + * @see #notifyFolderRenamedListeners + */ + protected void notifyFolderListeners(int type, Folder folder) { + if (folderListeners == null) + return; + + FolderEvent e = new FolderEvent(this, folder, type); + queueEvent(e, folderListeners); + } + + /** + * Notify all FolderListeners about the renaming of a folder. + * Store implementations are expected to use this method to broadcast + * Folder events indicating the renaming of folders.

+ *

+ * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * FolderListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param oldF the folder being renamed + * @param newF the folder representing the new name. + * @since JavaMail 1.1 + */ + protected void notifyFolderRenamedListeners(Folder oldF, Folder newF) { + if (folderListeners == null) + return; + + FolderEvent e = new FolderEvent(this, oldF, newF, FolderEvent.RENAMED); + queueEvent(e, folderListeners); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/StoreClosedException.java b/net-mail/src/main/java/jakarta/mail/StoreClosedException.java new file mode 100644 index 0000000..031490d --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/StoreClosedException.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +/** + * This exception is thrown when a method is invoked on a Messaging object + * and the Store that owns that object has died due to some reason. + * This exception should be treated as a fatal error; in particular any + * messaging object belonging to that Store must be considered invalid.

+ *

+ * The connect method may be invoked on the dead Store object to + * revive it.

+ *

+ * The getMessage() method returns more detailed information about the + * error that caused this exception. + * + * @author John Mani + */ +@SuppressWarnings("serial") +public class StoreClosedException extends MessagingException { + transient private Store store; + + /** + * Constructs a StoreClosedException with no detail message. + * + * @param store The dead Store object + */ + public StoreClosedException(Store store) { + this(store, null); + } + + /** + * Constructs a StoreClosedException with the specified + * detail message. + * + * @param store The dead Store object + * @param message The detailed error message + */ + public StoreClosedException(Store store, String message) { + super(message); + this.store = store; + } + + /** + * Constructs a StoreClosedException with the specified + * detail message and embedded exception. The exception is chained + * to this exception. + * + * @param store The dead Store object + * @param message The detailed error message + * @param e The embedded exception + * @since JavaMail 1.5 + */ + public StoreClosedException(Store store, String message, Exception e) { + super(message, e); + this.store = store; + } + + /** + * Returns the dead Store object. + * + * @return the dead Store object + */ + public Store getStore() { + return store; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/Transport.java b/net-mail/src/main/java/jakarta/mail/Transport.java new file mode 100644 index 0000000..645640d --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/Transport.java @@ -0,0 +1,397 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import jakarta.mail.event.TransportEvent; +import jakarta.mail.event.TransportListener; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +/** + * An abstract class that models a message transport. + * Subclasses provide actual implementations.

+ *

+ * Note that Transport extends the Service + * class, which provides many common methods for naming transports, + * connecting to transports, and listening to connection events. + * + * @author John Mani + * @author Max Spivak + * @author Bill Shannon + * @see Service + * @see jakarta.mail.event.ConnectionEvent + * @see TransportEvent + */ + +public abstract class Transport extends Service { + + // Vector of Transport listeners + private volatile Vector transportListeners = null; + + /** + * Constructor. + * + * @param session Session object for this Transport. + * @param urlname URLName object to be used for this Transport + */ + public Transport(Session session, URLName urlname) { + super(session, urlname); + } + + /** + * Send a message. The message will be sent to all recipient + * addresses specified in the message (as returned from the + * Message method getAllRecipients), + * using message transports appropriate to each address. The + * send method calls the saveChanges + * method on the message before sending it.

+ *

+ * If any of the recipient addresses is detected to be invalid by + * the Transport during message submission, a SendFailedException + * is thrown. Clients can get more detail about the failure by examining + * the exception. Whether or not the message is still sent successfully + * to any valid addresses depends on the Transport implementation. See + * SendFailedException for more details. Note also that success does + * not imply that the message was delivered to the ultimate recipient, + * as failures may occur in later stages of delivery. Once a Transport + * accepts a message for delivery to a recipient, failures that occur later + * should be reported to the user via another mechanism, such as + * returning the undeliverable message.

+ *

+ * In typical usage, a SendFailedException reflects an error detected + * by the server. The details of the SendFailedException will usually + * contain the error message from the server (such as an SMTP error + * message). An address may be detected as invalid for a variety of + * reasons - the address may not exist, the address may have invalid + * syntax, the address may have exceeded its quota, etc.

+ *

+ * Note that send is a static method that creates and + * manages its own connection. Any connection associated with any + * Transport instance used to invoke this method is ignored and not + * used. This method should only be invoked using the form + * Transport.send(msg);, and should never be invoked + * using an instance variable. + * + * @param msg the message to send + * @throws SendFailedException if the message could not + * be sent to some or any of the recipients. + * @throws MessagingException for other failures + * @see Message#saveChanges + * @see Message#getAllRecipients + * @see #send(Message, Address[]) + * @see SendFailedException + */ + public static void send(Message msg) throws MessagingException { + msg.saveChanges(); // do this first + send0(msg, msg.getAllRecipients(), null, null); + } + + /** + * Send the message to the specified addresses, ignoring any + * recipients specified in the message itself. The + * send method calls the saveChanges + * method on the message before sending it. + * + * @param msg the message to send + * @param addresses the addresses to which to send the message + * @throws SendFailedException if the message could not + * be sent to some or any of the recipients. + * @throws MessagingException for other failures + * @see Message#saveChanges + * @see SendFailedException + * @see #send(Message) + */ + public static void send(Message msg, Address[] addresses) + throws MessagingException { + + msg.saveChanges(); + send0(msg, addresses, null, null); + } + + /** + * Send a message. The message will be sent to all recipient + * addresses specified in the message (as returned from the + * Message method getAllRecipients). + * The send method calls the saveChanges + * method on the message before sending it.

+ *

+ * Use the specified user name and password to authenticate to + * the mail server. + * + * @param msg the message to send + * @param user the user name + * @param password this user's password + * @throws SendFailedException if the message could not + * be sent to some or any of the recipients. + * @throws MessagingException for other failures + * @see Message#saveChanges + * @see SendFailedException + * @see #send(Message) + * @since JavaMail 1.5 + */ + public static void send(Message msg, + String user, String password) throws MessagingException { + + msg.saveChanges(); + send0(msg, msg.getAllRecipients(), user, password); + } + + /** + * Send the message to the specified addresses, ignoring any + * recipients specified in the message itself. The + * send method calls the saveChanges + * method on the message before sending it.

+ *

+ * Use the specified user name and password to authenticate to + * the mail server. + * + * @param msg the message to send + * @param addresses the addresses to which to send the message + * @param user the user name + * @param password this user's password + * @throws SendFailedException if the message could not + * be sent to some or any of the recipients. + * @throws MessagingException for other failures + * @see Message#saveChanges + * @see SendFailedException + * @see #send(Message) + * @since JavaMail 1.5 + */ + public static void send(Message msg, Address[] addresses, + String user, String password) throws MessagingException { + + msg.saveChanges(); + send0(msg, addresses, user, password); + } + + // send, but without the saveChanges + private static void send0(Message msg, Address[] addresses, + String user, String password) throws MessagingException { + + if (addresses == null || addresses.length == 0) + throw new SendFailedException("No recipient addresses"); + + /* + * protocols is a map containing the addresses + * indexed by address type + */ + Map> protocols + = new HashMap<>(); + + // Lists of addresses + List

invalid = new ArrayList<>(); + List
validSent = new ArrayList<>(); + List
validUnsent = new ArrayList<>(); + + for (int i = 0; i < addresses.length; i++) { + // is this address type already in the map? + if (protocols.containsKey(addresses[i].getType())) { + List
v = protocols.get(addresses[i].getType()); + v.add(addresses[i]); + } else { + // need to add a new protocol + List
w = new ArrayList<>(); + w.add(addresses[i]); + protocols.put(addresses[i].getType(), w); + } + } + + int dsize = protocols.size(); + if (dsize == 0) + throw new SendFailedException("No recipient addresses"); + + Session s = (msg.session != null) ? msg.session : + Session.getDefaultInstance(System.getProperties(), null); + Transport transport; + + /* + * Optimize the case of a single protocol. + */ + if (dsize == 1) { + transport = s.getTransport(addresses[0]); + try { + if (user != null) + transport.connect(user, password); + else + transport.connect(); + transport.sendMessage(msg, addresses); + } finally { + transport.close(); + } + return; + } + + /* + * More than one protocol. Have to do them one at a time + * and collect addresses and chain exceptions. + */ + MessagingException chainedEx = null; + boolean sendFailed = false; + + for (List
v : protocols.values()) { + Address[] protaddresses = new Address[v.size()]; + v.toArray(protaddresses); + + // Get a Transport that can handle this address type. + if ((transport = s.getTransport(protaddresses[0])) == null) { + // Could not find an appropriate Transport .. + // Mark these addresses invalid. + Collections.addAll(invalid, protaddresses); + continue; + } + try { + transport.connect(); + transport.sendMessage(msg, protaddresses); + } catch (SendFailedException sex) { + sendFailed = true; + // chain the exception we're catching to any previous ones + if (chainedEx == null) + chainedEx = sex; + else + chainedEx.setNextException(sex); + + // retrieve invalid addresses + Address[] a = sex.getInvalidAddresses(); + if (a != null) + Collections.addAll(invalid, a); + + // retrieve validSent addresses + a = sex.getValidSentAddresses(); + if (a != null) + Collections.addAll(validSent, a); + + // retrieve validUnsent addresses + Address[] c = sex.getValidUnsentAddresses(); + if (c != null) + Collections.addAll(validUnsent, c); + } catch (MessagingException mex) { + sendFailed = true; + // chain the exception we're catching to any previous ones + if (chainedEx == null) + chainedEx = mex; + else + chainedEx.setNextException(mex); + } finally { + transport.close(); + } + } + + // done with all protocols. throw exception if something failed + if (sendFailed || invalid.size() != 0 || validUnsent.size() != 0) { + Address[] a = null, b = null, c = null; + + // copy address lists into arrays + if (validSent.size() > 0) { + a = new Address[validSent.size()]; + validSent.toArray(a); + } + if (validUnsent.size() > 0) { + b = new Address[validUnsent.size()]; + validUnsent.toArray(b); + } + if (invalid.size() > 0) { + c = new Address[invalid.size()]; + invalid.toArray(c); + } + throw new SendFailedException("Sending failed", chainedEx, + a, b, c); + } + } + + /** + * Send the Message to the specified list of addresses. An appropriate + * TransportEvent indicating the delivery status is delivered to any + * TransportListener registered on this Transport. Also, if any of + * the addresses is invalid, a SendFailedException is thrown. + * Whether or not the message is still sent succesfully to + * any valid addresses depends on the Transport implementation.

+ *

+ * Unlike the static send method, the sendMessage + * method does not call the saveChanges method on + * the message; the caller should do so. + * + * @param msg The Message to be sent + * @param addresses array of addresses to send this message to + * @throws SendFailedException if the send failed because of + * invalid addresses. + * @throws MessagingException if the connection is dead or not in the + * connected state + * @see TransportEvent + */ + public abstract void sendMessage(Message msg, Address[] addresses) + throws MessagingException; + + /** + * Add a listener for Transport events.

+ *

+ * The default implementation provided here adds this listener + * to an internal list of TransportListeners. + * + * @param l the Listener for Transport events + * @see TransportEvent + */ + public synchronized void addTransportListener(TransportListener l) { + if (transportListeners == null) + transportListeners = new Vector<>(); + transportListeners.addElement(l); + } + + /** + * Remove a listener for Transport events.

+ *

+ * The default implementation provided here removes this listener + * from the internal list of TransportListeners. + * + * @param l the listener + * @see #addTransportListener + */ + public synchronized void removeTransportListener(TransportListener l) { + if (transportListeners != null) + transportListeners.removeElement(l); + } + + /** + * Notify all TransportListeners. Transport implementations are + * expected to use this method to broadcast TransportEvents.

+ *

+ * The provided default implementation queues the event into + * an internal event queue. An event dispatcher thread dequeues + * events from the queue and dispatches them to the registered + * TransportListeners. Note that the event dispatching occurs + * in a separate thread, thus avoiding potential deadlock problems. + * + * @param type the TransportEvent type + * @param validSent valid addresses to which message was sent + * @param validUnsent valid addresses to which message was not sent + * @param invalid the invalid addresses + * @param msg the message + */ + protected void notifyTransportListeners(int type, Address[] validSent, + Address[] validUnsent, + Address[] invalid, Message msg) { + if (transportListeners == null) + return; + + TransportEvent e = new TransportEvent(this, type, validSent, + validUnsent, invalid, msg); + queueEvent(e, transportListeners); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/UIDFolder.java b/net-mail/src/main/java/jakarta/mail/UIDFolder.java new file mode 100644 index 0000000..474d96a --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/UIDFolder.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import java.util.NoSuchElementException; + +/** + * The UIDFolder interface is implemented by Folders + * that can support the "disconnected" mode of operation, by providing + * unique-ids for messages in the folder. This interface is based on + * the IMAP model for supporting disconnected operation.

+ *

+ * A Unique identifier (UID) is a positive long value, assigned to + * each message in a specific folder. Unique identifiers are assigned + * in a strictly ascending fashion in the mailbox. + * That is, as each message is added to the mailbox it is assigned a + * higher UID than the message(s) which were added previously. Unique + * identifiers persist across sessions. This permits a client to + * resynchronize its state from a previous session with the server.

+ *

+ * Associated with every mailbox is a unique identifier validity value. + * If unique identifiers from an earlier session fail to persist to + * this session, the unique identifier validity value + * must be greater than the one used in the earlier + * session.

+ *

+ * Refer to RFC 2060 + * for more information. + *

+ * All the Folder objects returned by the default IMAP provider implement + * the UIDFolder interface. Use it as follows: + *

+ *
+ * 	Folder f = store.getFolder("whatever");
+ * 	UIDFolder uf = (UIDFolder)f;
+ * 	long uid = uf.getUID(msg);
+ *
+ * 
+ * + * @author Bill Shannon + * @author John Mani + */ + +public interface UIDFolder { + + /** + * This is a special value that can be used as the end + * parameter in getMessagesByUID(start, end), to denote the + * UID of the last message in the folder. + * + * @see #getMessagesByUID + */ + long LASTUID = -1; + /** + * The largest value possible for a UID, a 32-bit unsigned integer. + * This can be used to fetch all new messages by keeping track of the + * last UID that was seen and using: + *
+     *
+     * 	Folder f = store.getFolder("whatever");
+     * 	UIDFolder uf = (UIDFolder)f;
+     * 	Message[] newMsgs =
+     * 		uf.getMessagesByUID(lastSeenUID + 1, UIDFolder.MAXUID);
+     *
+     * 
+ * + * @since JavaMail 1.6 + */ + long MAXUID = 0xffffffffL; // max 32-bit unsigned int + + /** + * Returns the UIDValidity value associated with this folder.

+ *

+ * Clients typically compare this value against a UIDValidity + * value saved from a previous session to insure that any cached + * UIDs are not stale. + * + * @return UIDValidity + * @throws MessagingException for failures + */ + long getUIDValidity() throws MessagingException; + + /** + * Get the Message corresponding to the given UID. If no such + * message exists, null is returned. + * + * @param uid UID for the desired message + * @return the Message object. null is returned + * if no message corresponding to this UID is obtained. + * @throws MessagingException for failures + */ + Message getMessageByUID(long uid) throws MessagingException; + + /** + * Get the Messages specified by the given range. The special + * value LASTUID can be used for the end parameter + * to indicate the UID of the last message in the folder.

+ *

+ * Note that end need not be greater than start; + * the order of the range doesn't matter. + * Note also that, unless the folder is empty, use of LASTUID ensures + * that at least one message will be returned - the last message in the + * folder. + * + * @param start start UID + * @param end end UID + * @return array of Message objects + * @throws MessagingException for failures + * @see #LASTUID + */ + Message[] getMessagesByUID(long start, long end) + throws MessagingException; + + /** + * Get the Messages specified by the given array of UIDs. If any UID is + * invalid, null is returned for that entry.

+ *

+ * Note that the returned array will be of the same size as the specified + * array of UIDs, and null entries may be present in the + * array to indicate invalid UIDs. + * + * @param uids array of UIDs + * @return array of Message objects + * @throws MessagingException for failures + */ + Message[] getMessagesByUID(long[] uids) + throws MessagingException; + + /** + * Get the UID for the specified message. Note that the message + * must belong to this folder. Otherwise + * java.util.NoSuchElementException is thrown. + * + * @param message Message from this folder + * @return UID for this message + * @throws NoSuchElementException if the given Message + * is not in this Folder. + * @throws MessagingException for other failures + */ + long getUID(Message message) throws MessagingException; + + /** + * Returns the predicted UID that will be assigned to the + * next message that is appended to this folder. + * Messages might be appended to the folder after this value + * is retrieved, causing this value to be out of date. + * This value might only be updated when a folder is first opened. + * Note that messages may have been appended to the folder + * while it was open and thus this value may be out of + * date.

+ *

+ * If the value is unknown, -1 is returned. + * + * @return the UIDNEXT value, or -1 if unknown + * @throws MessagingException for failures + * @since JavaMail 1.6 + */ + long getUIDNext() throws MessagingException; + + /** + * A fetch profile item for fetching UIDs. + * This inner class extends the FetchProfile.Item + * class to add new FetchProfile item types, specific to UIDFolders. + * The only item currently defined here is the UID item. + * + * @see FetchProfile + */ + class FetchProfileItem extends FetchProfile.Item { + /** + * UID is a fetch profile item that can be included in a + * FetchProfile during a fetch request to a Folder. + * This item indicates that the UIDs 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(UIDFolder.FetchProfileItem.UID);
+         * 	folder.fetch(msgs, fp);
+         *
+         * 
+ */ + public static final FetchProfileItem UID = + new FetchProfileItem("UID"); + + /** + * Constructor for an item. + * + * @param name the item name + */ + protected FetchProfileItem(String name) { + super(name); + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/URLName.java b/net-mail/src/main/java/jakarta/mail/URLName.java new file mode 100644 index 0000000..1fb091a --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/URLName.java @@ -0,0 +1,774 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.BitSet; +import java.util.Locale; +import java.util.Objects; + + +/** + * The name of a URL. This class represents a URL name and also + * provides the basic parsing functionality to parse most internet + * standard URL schemes.

+ *

+ * Note that this class differs from java.net.URL + * in that this class just represents the name of a URL, it does + * not model the connection to a URL. + * + * @author Christopher Cotton + * @author Bill Shannon + */ + +public class URLName { + + static final int caseDiff = ('a' - 'A'); + /** + * The class contains a utility method for converting a + * String into a MIME format called + * "x-www-form-urlencoded" format. + *

+ * To convert a String, each character is examined in turn: + *

    + *
  • The ASCII characters 'a' through 'z', + * 'A' through 'Z', '0' + * through '9', and ".", "-", + * "*", "_" remain the same. + *
  • The space character ' ' is converted into a + * plus sign '+'. + *
  • All other characters are converted into the 3-character string + * "%xy", where xy is the two-digit + * hexadecimal representation of the lower 8-bits of the character. + *
+ * + * @author Herb Jellinek + * @since JDK1.0 + */ + static BitSet dontNeedEncoding; + /** + * A way to turn off encoding, just in case... + */ + private static boolean doEncode = true; + + static { + try { + doEncode = !Boolean.getBoolean("mail.URLName.dontencode"); + } catch (Exception ex) { + // ignore any errors + } + } + + static { + dontNeedEncoding = new BitSet(256); + int i; + for (i = 'a'; i <= 'z'; i++) { + dontNeedEncoding.set(i); + } + for (i = 'A'; i <= 'Z'; i++) { + dontNeedEncoding.set(i); + } + for (i = '0'; i <= '9'; i++) { + dontNeedEncoding.set(i); + } + /* encoding a space to a + is done in the encode() method */ + dontNeedEncoding.set(' '); + dontNeedEncoding.set('-'); + dontNeedEncoding.set('_'); + dontNeedEncoding.set('.'); + dontNeedEncoding.set('*'); + } + + /** + * The full version of the URL + */ + protected String fullURL; + /** + * The protocol to use (ftp, http, nntp, imap, pop3 ... etc.) . + */ + private String protocol; + /** + * The username to use when connecting + */ + private String username; + /** + * The password to use when connecting. + */ + private String password; + /** + * The host name to which to connect. + */ + private String host; + /** + * The host's IP address, used in equals and hashCode. + * Computed on demand. + */ + private InetAddress hostAddress; + private boolean hostAddressKnown = false; + /** + * The protocol port to connect to. + */ + private int port = -1; + /** + * The specified file name on that host. + */ + private String file; + /** + * # reference. + */ + private String ref; + /** + * Our hash code. + */ + private int hashCode = 0; + + /** + * Creates a URLName object from the specified protocol, + * host, port number, file, username, and password. Specifying a port + * number of -1 indicates that the URL should use the default port for + * the protocol. + * + * @param protocol the protocol + * @param host the host name + * @param port the port number + * @param file the file + * @param username the user name + * @param password the password + */ + public URLName( + String protocol, + String host, + int port, + String file, + String username, + String password + ) { + this.protocol = protocol; + this.host = host; + this.port = port; + int refStart; + if (file != null && (refStart = file.indexOf('#')) != -1) { + this.file = file.substring(0, refStart); + this.ref = file.substring(refStart + 1); + } else { + this.file = file; + this.ref = null; + } + this.username = doEncode ? encode(username) : username; + this.password = doEncode ? encode(password) : password; + } + + /** + * Construct a URLName from a java.net.URL object. + * + * @param url the URL + */ + public URLName(URL url) { + this(url.toString()); + } + + /** + * Construct a URLName from the string. Parses out all the possible + * information (protocol, host, port, file, username, password). + * + * @param url the URL string + */ + public URLName(String url) { + parseString(url); + } + + /** + * Translates a string into x-www-form-urlencoded format. + * + * @param s String to be translated. + * @return the translated String. + */ + static String encode(String s) { + if (s == null) + return null; + // the common case is no encoding is needed + for (int i = 0; i < s.length(); i++) { + int c = s.charAt(i); + if (c == ' ' || !dontNeedEncoding.get(c)) + return _encode(s); + } + return s; + } + + private static String _encode(String s) { + int maxBytesPerChar = 10; + StringBuilder out = new StringBuilder(s.length()); + ByteArrayOutputStream buf = new ByteArrayOutputStream(maxBytesPerChar); + OutputStreamWriter writer = new OutputStreamWriter(buf); + + for (int i = 0; i < s.length(); i++) { + int c = s.charAt(i); + if (dontNeedEncoding.get(c)) { + if (c == ' ') { + c = '+'; + } + out.append((char) c); + } else { + // convert to external encoding before hex conversion + try { + writer.write(c); + writer.flush(); + } catch (IOException e) { + buf.reset(); + continue; + } + byte[] ba = buf.toByteArray(); + for (int j = 0; j < ba.length; j++) { + out.append('%'); + char ch = Character.forDigit((ba[j] >> 4) & 0xF, 16); + // converting to use uppercase letter as part of + // the hex value if ch is a letter. + if (Character.isLetter(ch)) { + ch -= caseDiff; + } + out.append(ch); + ch = Character.forDigit(ba[j] & 0xF, 16); + if (Character.isLetter(ch)) { + ch -= caseDiff; + } + out.append(ch); + } + buf.reset(); + } + } + + return out.toString(); + } + + /** + * Decodes a "x-www-form-urlencoded" + * to a String. + * + * @param s the String to decode + * @return the newly decoded String + */ + static String decode(String s) { + if (s == null) + return null; + if (indexOfAny(s, "+%") == -1) + return s; // the common case + + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + switch (c) { + case '+': + sb.append(' '); + break; + case '%': + try { + sb.append((char) Integer.parseInt( + s.substring(i + 1, i + 3), 16)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + "Illegal URL encoded value: " + + s.substring(i, i + 3)); + } + i += 2; + break; + default: + sb.append(c); + break; + } + } + // Undo conversion to external encoding + String result = sb.toString(); + byte[] inputBytes = result.getBytes(StandardCharsets.ISO_8859_1); + result = new String(inputBytes); + return result; + } + + /** + * Return the first index of any of the characters in "any" in "s", + * or -1 if none are found. + *

+ * This should be a method on String. + */ + private static int indexOfAny(String s, String any) { + return indexOfAny(s, any, 0); + } + + private static int indexOfAny(String s, String any, int start) { + try { + int len = s.length(); + for (int i = start; i < len; i++) { + if (any.indexOf(s.charAt(i)) >= 0) + return i; + } + return -1; + } catch (StringIndexOutOfBoundsException e) { + return -1; + } + } + + /** + * Constructs a string representation of this URLName. + */ + @Override + public String toString() { + if (fullURL == null) { + // add the "protocol:" + StringBuilder tempURL = new StringBuilder(); + if (protocol != null) { + tempURL.append(protocol); + tempURL.append(":"); + } + + if (username != null || host != null) { + // add the "//" + tempURL.append("//"); + + // add the user:password@ + // XXX - can you just have a password? without a username? + if (username != null) { + tempURL.append(username); + + if (password != null) { + tempURL.append(":"); + tempURL.append(password); + } + + tempURL.append("@"); + } + + // add host + if (host != null) { + tempURL.append(host); + } + + // add port (if needed) + if (port != -1) { + tempURL.append(":"); + tempURL.append(port); + } + if (file != null) + tempURL.append("/"); + } + + // add the file + if (file != null) { + tempURL.append(file); + } + + // add the ref + if (ref != null) { + tempURL.append("#"); + tempURL.append(ref); + } + + // create the fullURL now + fullURL = tempURL.toString(); + } + + return fullURL; + } + + /** + * Method which does all of the work of parsing the string. + * + * @param url the URL string to parse + */ + private void parseString(String url) { + // initialize everything in case called from subclass + // (URLName really should be a final class) + protocol = file = ref = host = username = password = null; + port = -1; + + int len = url.length(); + + // find the protocol + // XXX - should check for only legal characters before the colon + // (legal: a-z, A-Z, 0-9, "+", ".", "-") + int protocolEnd = url.indexOf(':'); + if (protocolEnd != -1) + protocol = url.substring(0, protocolEnd); + + // is this an Internet standard URL that contains a host name? + if (url.regionMatches(protocolEnd + 1, "//", 0, 2)) { + // find where the file starts + String fullhost = null; + int fileStart = url.indexOf('/', protocolEnd + 3); + if (fileStart != -1) { + fullhost = url.substring(protocolEnd + 3, fileStart); + if (fileStart + 1 < len) + file = url.substring(fileStart + 1); + else + file = ""; + } else + fullhost = url.substring(protocolEnd + 3); + + // examine the fullhost, for username password etc. + int i = fullhost.indexOf('@'); + if (i != -1) { + String fulluserpass = fullhost.substring(0, i); + fullhost = fullhost.substring(i + 1); + + // get user and password + int passindex = fulluserpass.indexOf(':'); + if (passindex != -1) { + username = fulluserpass.substring(0, passindex); + password = fulluserpass.substring(passindex + 1); + } else { + username = fulluserpass; + } + } + + // get the port (if there) + int portindex; + if (fullhost.length() > 0 && fullhost.charAt(0) == '[') { + // an IPv6 address? + portindex = fullhost.indexOf(':', fullhost.indexOf(']')); + } else { + portindex = fullhost.indexOf(':'); + } + if (portindex != -1) { + String portstring = fullhost.substring(portindex + 1); + if (portstring.length() > 0) { + try { + port = Integer.parseInt(portstring); + } catch (NumberFormatException nfex) { + port = -1; + } + } + + host = fullhost.substring(0, portindex); + } else { + host = fullhost; + } + } else { + if (protocolEnd + 1 < len) + file = url.substring(protocolEnd + 1); + } + + // extract the reference from the file name, if any + int refStart; + if (file != null && (refStart = file.indexOf('#')) != -1) { + ref = file.substring(refStart + 1); + file = file.substring(0, refStart); + } + } + + /** + * Returns the port number of this URLName. + * Returns -1 if the port is not set. + * + * @return the port number + */ + public int getPort() { + return port; + } + + /** + * Returns the protocol of this URLName. + * Returns null if this URLName has no protocol. + * + * @return the protocol + */ + public String getProtocol() { + return protocol; + } + + /** + * Returns the file name of this URLName. + * Returns null if this URLName has no file name. + * + * @return the file name of this URLName + */ + public String getFile() { + return file; + } + + /** + * Returns the reference of this URLName. + * Returns null if this URLName has no reference. + * + * @return the reference part of the URLName + */ + public String getRef() { + return ref; + } + + /** + * Returns the host of this URLName. + * Returns null if this URLName has no host. + * + * @return the host name + */ + public String getHost() { + return host; + } + + /* The list of characters that are not encoded have been determined by + referencing O'Reilly's "HTML: The Definitive Guide" (page 164). */ + + /** + * Returns the user name of this URLName. + * Returns null if this URLName has no user name. + * + * @return the user name + */ + public String getUsername() { + return doEncode ? decode(username) : username; + } + + /** + * Returns the password of this URLName. + * Returns null if this URLName has no password. + * + * @return the password + */ + public String getPassword() { + return doEncode ? decode(password) : password; + } + + /** + * Constructs a URL from the URLName. + * + * @return the URL + * @throws MalformedURLException if the URL is malformed + */ + public URL getURL() throws MalformedURLException { + // URL expects the file to include the separating "/" + String f = getFile(); + if (f == null) + f = ""; + else + f = "/" + f; + return URI.create(getProtocol() + "://" + getHost() + ":" + getPort() + "/" + f).toURL(); + } + + + /* + * The class contains a utility method for converting from + * a MIME format called "x-www-form-urlencoded" + * to a String + *

+ * To convert to a String, each character is examined in turn: + *

    + *
  • The ASCII characters 'a' through 'z', + * 'A' through 'Z', and '0' + * through '9' remain the same. + *
  • The plus sign '+'is converted into a + * space character ' '. + *
  • The remaining characters are represented by 3-character + * strings which begin with the percent sign, + * "%xy", where xy is the two-digit + * hexadecimal representation of the lower 8-bits of the character. + *
+ * + * @author Mark Chamness + * @author Michael McCloskey + * @since 1.2 + */ + + /** + * Compares two URLNames. The result is true if and only if the + * argument is not null and is a URLName object that represents the + * same URLName as this object. Two URLName objects are equal if + * they have the same protocol and the same host, + * the same port number on the host, the same username, + * and the same file on the host. The fields (host, username, + * file) are also considered the same if they are both + * null.

+ *

+ * Hosts are considered equal if the names are equal (case independent) + * or if host name lookups for them both succeed and they both reference + * the same IP address.

+ *

+ * Note that URLName has no knowledge of default port numbers for + * particular protocols, so "imap://host" and "imap://host:143" + * would not compare as equal.

+ *

+ * Note also that the password field is not included in the comparison, + * nor is any reference field appended to the filename. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof URLName)) + return false; + URLName u2 = (URLName) obj; + + // compare protocols + if (!(Objects.equals(protocol, u2.protocol))) + return false; + + // compare hosts + InetAddress a1 = getHostAddress(), a2 = u2.getHostAddress(); + // if we have internet address for both, and they're not the same, fail + if (a1 != null && a2 != null) { + if (!a1.equals(a2)) + return false; + // else, if we have host names for both, and they're not the same, fail + } else if (host != null && u2.host != null) { + if (!host.equalsIgnoreCase(u2.host)) + return false; + // else, if not both null + } else if (host != u2.host) { + return false; + } + // at this point, hosts match + + // compare usernames + if (!(Objects.equals(username, u2.username))) + return false; + + // Forget about password since it doesn't + // really denote a different store. + + // compare files + String f1 = file == null ? "" : file; + String f2 = u2.file == null ? "" : u2.file; + + if (!f1.equals(f2)) + return false; + + // compare ports + if (port != u2.port) + return false; + + // all comparisons succeeded, they're equal + return true; + } + + /** + * Compute the hash code for this URLName. + */ + @Override + public int hashCode() { + if (hashCode != 0) + return hashCode; + if (protocol != null) + hashCode += protocol.hashCode(); + InetAddress addr = getHostAddress(); + if (addr != null) + hashCode += addr.hashCode(); + else if (host != null) + hashCode += host.toLowerCase(Locale.ENGLISH).hashCode(); + if (username != null) + hashCode += username.hashCode(); + if (file != null) + hashCode += file.hashCode(); + hashCode += port; + return hashCode; + } + + /** + * Get the IP address of our host. Look up the + * name the first time and remember that we've done + * so, whether the lookup fails or not. + */ + private synchronized InetAddress getHostAddress() { + if (hostAddressKnown) + return hostAddress; + if (host == null) + return null; + try { + hostAddress = InetAddress.getByName(host); + } catch (UnknownHostException ex) { + hostAddress = null; + } + hostAddressKnown = true; + return hostAddress; + } + + /* + // Do not remove, this is needed when testing new URL cases + public static void main(String[] argv) { + String [] testURLNames = { + "protocol://userid:password@host:119/file", + "http://funny/folder/file.html", + "http://funny/folder/file.html#ref", + "http://funny/folder/file.html#", + "http://funny/#ref", + "imap://jmr:secret@labyrinth//var/mail/jmr", + "nntp://fred@labyrinth:143/save/it/now.mbox", + "imap://jmr@labyrinth/INBOX", + "imap://labryrinth", + "imap://labryrinth/", + "file:", + "file:INBOX", + "file:/home/shannon/mail/foo", + "/tmp/foo", + "//host/tmp/foo", + ":/tmp/foo", + "/really/weird:/tmp/foo#bar", + "" + }; + + URLName url = + new URLName("protocol", "host", 119, "file", "userid", "password"); + System.out.println("Test URL: " + url.toString()); + if (argv.length == 0) { + for (int i = 0; i < testURLNames.length; i++) { + print(testURLNames[i]); + System.out.println(); + } + } else { + for (int i = 0; i < argv.length; i++) { + print(argv[i]); + System.out.println(); + } + if (argv.length == 2) { + URLName u1 = new URLName(argv[0]); + URLName u2 = new URLName(argv[1]); + System.out.println("URL1 hash code: " + u1.hashCode()); + System.out.println("URL2 hash code: " + u2.hashCode()); + if (u1.equals(u2)) + System.out.println("success, equal"); + else + System.out.println("fail, not equal"); + if (u2.equals(u1)) + System.out.println("success, equal"); + else + System.out.println("fail, not equal"); + if (u1.hashCode() == u2.hashCode()) + System.out.println("success, hashCodes equal"); + else + System.out.println("fail, hashCodes not equal"); + } + } + } + + private static void print(String name) { + URLName url = new URLName(name); + System.out.println("Original URL: " + name); + System.out.println("The fullUrl : " + url.toString()); + if (!name.equals(url.toString())) + System.out.println(" : NOT EQUAL!"); + System.out.println("The protocol is: " + url.getProtocol()); + System.out.println("The host is: " + url.getHost()); + System.out.println("The port is: " + url.getPort()); + System.out.println("The user is: " + url.getUsername()); + System.out.println("The password is: " + url.getPassword()); + System.out.println("The file is: " + url.getFile()); + System.out.println("The ref is: " + url.getRef()); + } + */ +} diff --git a/net-mail/src/main/java/jakarta/mail/event/ConnectionAdapter.java b/net-mail/src/main/java/jakarta/mail/event/ConnectionAdapter.java new file mode 100644 index 0000000..5521cd9 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/ConnectionAdapter.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +/** + * The adapter which receives connection events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class ConnectionAdapter implements ConnectionListener { + + + /** + * Creates a default {@code ConnectionAdapter}. + */ + public ConnectionAdapter() { + } + + @Override + public void opened(ConnectionEvent e) { + } + + @Override + public void disconnected(ConnectionEvent e) { + } + + @Override + public void closed(ConnectionEvent e) { + } +} diff --git a/net-mail/src/main/java/jakarta/mail/event/ConnectionEvent.java b/net-mail/src/main/java/jakarta/mail/event/ConnectionEvent.java new file mode 100644 index 0000000..513fdb3 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/ConnectionEvent.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +/** + * This class models Connection events. + * + * @author John Mani + */ +@SuppressWarnings("serial") +public class ConnectionEvent extends MailEvent { + + /** + * A connection was opened. + */ + public static final int OPENED = 1; + /** + * A connection was disconnected (not currently used). + */ + public static final int DISCONNECTED = 2; + /** + * A connection was closed. + */ + public static final int CLOSED = 3; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * Construct a ConnectionEvent. + * + * @param source The source object + * @param type the event type + */ + public ConnectionEvent(Object source, int type) { + super(source); + this.type = type; + } + + /** + * Return the type of this event + * + * @return type + */ + public int getType() { + return type; + } + + /** + * Invokes the appropriate ConnectionListener method + */ + @Override + public void dispatch(Object listener) { + if (type == OPENED) + ((ConnectionListener) listener).opened(this); + else if (type == DISCONNECTED) + ((ConnectionListener) listener).disconnected(this); + else if (type == CLOSED) + ((ConnectionListener) listener).closed(this); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/event/ConnectionListener.java b/net-mail/src/main/java/jakarta/mail/event/ConnectionListener.java new file mode 100644 index 0000000..2cdeb1d --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/ConnectionListener.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +/** + * This is the Listener interface for Connection events. + * + * @author John Mani + */ + +public interface ConnectionListener extends java.util.EventListener { + + /** + * Invoked when a Store/Folder/Transport is opened. + * + * @param e the ConnectionEvent + */ + void opened(ConnectionEvent e); + + /** + * Invoked when a Store is disconnected. Note that a folder + * cannot be disconnected, so a folder will not fire this event + * + * @param e the ConnectionEvent + */ + void disconnected(ConnectionEvent e); + + /** + * Invoked when a Store/Folder/Transport is closed. + * + * @param e the ConnectionEvent + */ + void closed(ConnectionEvent e); +} diff --git a/net-mail/src/main/java/jakarta/mail/event/FolderAdapter.java b/net-mail/src/main/java/jakarta/mail/event/FolderAdapter.java new file mode 100644 index 0000000..2e47de7 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/FolderAdapter.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +/** + * The adapter which receives Folder events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class FolderAdapter implements FolderListener { + + /** + * Creates a default {@code FolderAdapter}. + */ + public FolderAdapter() { + } + + @Override + public void folderCreated(FolderEvent e) { + } + + @Override + public void folderRenamed(FolderEvent e) { + } + + @Override + public void folderDeleted(FolderEvent e) { + } +} diff --git a/net-mail/src/main/java/jakarta/mail/event/FolderEvent.java b/net-mail/src/main/java/jakarta/mail/event/FolderEvent.java new file mode 100644 index 0000000..06ffc9b --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/FolderEvent.java @@ -0,0 +1,147 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +import jakarta.mail.Folder; + +/** + * This class models Folder existence events. FolderEvents are + * delivered to FolderListeners registered on the affected Folder as + * well as the containing Store.

+ *

+ * Service providers vary widely in their ability to notify clients of + * these events. At a minimum, service providers must notify listeners + * registered on the same Store or Folder object on which the operation + * occurs. Service providers may also notify listeners when changes + * are made through operations on other objects in the same virtual + * machine, or by other clients in the same or other hosts. Such + * notifications are not required and are typically not supported + * by mail protocols (including IMAP). + * + * @author John Mani + * @author Bill Shannon + */ +@SuppressWarnings("serial") +public class FolderEvent extends MailEvent { + + /** + * The folder was created. + */ + public static final int CREATED = 1; + /** + * The folder was deleted. + */ + public static final int DELETED = 2; + /** + * The folder was renamed. + */ + public static final int RENAMED = 3; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * The folder the event occurred on. + */ + transient protected Folder folder; + + /** + * The folder that represents the new name, in case of a RENAMED event. + * + * @since JavaMail 1.1 + */ + transient protected Folder newFolder; + + /** + * Constructor. + * + * @param source The source of the event + * @param folder The affected folder + * @param type The event type + */ + public FolderEvent(Object source, Folder folder, int type) { + this(source, folder, folder, type); + } + + /** + * Constructor. Use for RENAMED events. + * + * @param source The source of the event + * @param oldFolder The folder that is renamed + * @param newFolder The folder that represents the new name + * @param type The event type + * @since JavaMail 1.1 + */ + public FolderEvent(Object source, Folder oldFolder, + Folder newFolder, int type) { + super(source); + this.folder = oldFolder; + this.newFolder = newFolder; + this.type = type; + } + + /** + * Return the type of this event. + * + * @return type + */ + public int getType() { + return type; + } + + /** + * Return the affected folder. + * + * @return the affected folder + * @see #getNewFolder + */ + public Folder getFolder() { + return folder; + } + + /** + * If this event indicates that a folder is renamed, (i.e, the event type + * is RENAMED), then this method returns the Folder object representing the + * new name.

+ *

+ * The getFolder() method returns the folder that is renamed. + * + * @return Folder representing the new name. + * @see #getFolder + * @since JavaMail 1.1 + */ + public Folder getNewFolder() { + return newFolder; + } + + /** + * Invokes the appropriate FolderListener method + */ + @Override + public void dispatch(Object listener) { + if (type == CREATED) + ((FolderListener) listener).folderCreated(this); + else if (type == DELETED) + ((FolderListener) listener).folderDeleted(this); + else if (type == RENAMED) + ((FolderListener) listener).folderRenamed(this); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/event/FolderListener.java b/net-mail/src/main/java/jakarta/mail/event/FolderListener.java new file mode 100644 index 0000000..473dbfb --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/FolderListener.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +/** + * This is the Listener interface for Folder events. + * + * @author John Mani + */ + +public interface FolderListener extends java.util.EventListener { + /** + * Invoked when a Folder is created. + * + * @param e the FolderEvent + */ + void folderCreated(FolderEvent e); + + /** + * Invoked when a folder is deleted. + * + * @param e the FolderEvent + */ + void folderDeleted(FolderEvent e); + + /** + * Invoked when a folder is renamed. + * + * @param e the FolderEvent + */ + void folderRenamed(FolderEvent e); +} diff --git a/net-mail/src/main/java/jakarta/mail/event/MailEvent.java b/net-mail/src/main/java/jakarta/mail/event/MailEvent.java new file mode 100644 index 0000000..e0a01b1 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/MailEvent.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +import java.util.EventObject; + +/** + * Common base class for mail events, defining the dispatch method. + * + * @author Bill Shannon + */ +@SuppressWarnings("serial") +public abstract class MailEvent extends EventObject { + + /** + * Construct a MailEvent referring to the given source. + * + * @param source the source of the event + */ + public MailEvent(Object source) { + super(source); + } + + /** + * This method invokes the appropriate method on a listener for + * this event. Subclasses provide the implementation. + * + * @param listener the listener to invoke on + */ + public abstract void dispatch(Object listener); +} diff --git a/net-mail/src/main/java/jakarta/mail/event/MessageChangedEvent.java b/net-mail/src/main/java/jakarta/mail/event/MessageChangedEvent.java new file mode 100644 index 0000000..64775e9 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/MessageChangedEvent.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +import jakarta.mail.Message; + +/** + * This class models Message change events. + * + * @author John Mani + */ +@SuppressWarnings("serial") +public class MessageChangedEvent extends MailEvent { + + /** + * The message's flags changed. + */ + public static final int FLAGS_CHANGED = 1; + /** + * The message's envelope (headers, but not body) changed. + */ + public static final int ENVELOPE_CHANGED = 2; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * The message that changed. + */ + transient protected Message msg; + + /** + * Constructor. + * + * @param source The folder that owns the message + * @param type The change type + * @param msg The changed message + */ + public MessageChangedEvent(Object source, int type, Message msg) { + super(source); + this.msg = msg; + this.type = type; + } + + /** + * Return the type of this event. + * + * @return type + */ + public int getMessageChangeType() { + return type; + } + + /** + * Return the changed Message. + * + * @return the message + */ + public Message getMessage() { + return msg; + } + + /** + * Invokes the appropriate MessageChangedListener method. + */ + @Override + public void dispatch(Object listener) { + ((MessageChangedListener) listener).messageChanged(this); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/event/MessageChangedListener.java b/net-mail/src/main/java/jakarta/mail/event/MessageChangedListener.java new file mode 100644 index 0000000..4957049 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/MessageChangedListener.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +/** + * This is the Listener interface for MessageChanged events + * + * @author John Mani + */ + +public interface MessageChangedListener extends java.util.EventListener { + /** + * Invoked when a message is changed. The change-type specifies + * what changed. + * + * @param e the MessageChangedEvent + * @see MessageChangedEvent#FLAGS_CHANGED + * @see MessageChangedEvent#ENVELOPE_CHANGED + */ + void messageChanged(MessageChangedEvent e); +} diff --git a/net-mail/src/main/java/jakarta/mail/event/MessageCountAdapter.java b/net-mail/src/main/java/jakarta/mail/event/MessageCountAdapter.java new file mode 100644 index 0000000..c63ff0d --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/MessageCountAdapter.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +/** + * The adapter which receives MessageCount events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class MessageCountAdapter implements MessageCountListener { + + /** + * Creates a default {@code MessageCountAdapter}. + */ + public MessageCountAdapter() { + } + + @Override + public void messagesAdded(MessageCountEvent e) { + } + + @Override + public void messagesRemoved(MessageCountEvent e) { + } +} diff --git a/net-mail/src/main/java/jakarta/mail/event/MessageCountEvent.java b/net-mail/src/main/java/jakarta/mail/event/MessageCountEvent.java new file mode 100644 index 0000000..fbba495 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/MessageCountEvent.java @@ -0,0 +1,138 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +import jakarta.mail.Folder; +import jakarta.mail.Message; + +/** + * This class notifies changes in the number of messages in a folder.

+ *

+ * Note that some folder types may only deliver MessageCountEvents at + * certain times or after certain operations. IMAP in particular will + * only notify the client of MessageCountEvents when a client issues a + * new command. Refer to + * RFC 3501 + * for details. + * A client may want to "poll" the folder by occasionally calling the + * {@link Folder#getMessageCount getMessageCount} or + * {@link Folder#isOpen isOpen} methods + * to solicit any such notifications. + * + * @author John Mani + */ +@SuppressWarnings("serial") +public class MessageCountEvent extends MailEvent { + + /** + * The messages were added to their folder + */ + public static final int ADDED = 1; + /** + * The messages were removed from their folder + */ + public static final int REMOVED = 2; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * If true, this event is the result of an explicit + * expunge by this client, and the messages in this + * folder have been renumbered to account for this. + * If false, this event is the result of an expunge + * by external sources. + * + * @serial + */ + protected boolean removed; + + /** + * The messages. + */ + transient protected Message[] msgs; + + /** + * Constructor. + * + * @param folder The containing folder + * @param type The event type + * @param removed If true, this event is the result of an explicit + * expunge by this client, and the messages in this + * folder have been renumbered to account for this. + * If false, this event is the result of an expunge + * by external sources. + * @param msgs The messages added/removed + */ + public MessageCountEvent(Folder folder, int type, + boolean removed, Message[] msgs) { + super(folder); + this.type = type; + this.removed = removed; + this.msgs = msgs; + } + + /** + * Return the type of this event. + * + * @return type + */ + public int getType() { + return type; + } + + /** + * Indicates whether this event is the result of an explicit + * expunge by this client, or due to an expunge from external + * sources. If true, this event is due to an + * explicit expunge and hence all remaining messages in this + * folder have been renumbered. If false, this event + * is due to an external expunge.

+ *

+ * Note that this method is valid only if the type of this event + * is REMOVED + * + * @return true if the message has been removed + */ + public boolean isRemoved() { + return removed; + } + + /** + * Return the array of messages added or removed. + * + * @return array of messages + */ + public Message[] getMessages() { + return msgs; + } + + /** + * Invokes the appropriate MessageCountListener method. + */ + @Override + public void dispatch(Object listener) { + if (type == ADDED) + ((MessageCountListener) listener).messagesAdded(this); + else // REMOVED + ((MessageCountListener) listener).messagesRemoved(this); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/event/MessageCountListener.java b/net-mail/src/main/java/jakarta/mail/event/MessageCountListener.java new file mode 100644 index 0000000..f6ee0b0 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/MessageCountListener.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +/** + * This is the Listener interface for MessageCount events. + * + * @author John Mani + */ + +public interface MessageCountListener extends java.util.EventListener { + /** + * Invoked when messages are added into a folder. + * + * @param e the MessageCountEvent + */ + void messagesAdded(MessageCountEvent e); + + /** + * Invoked when messages are removed (expunged) from a folder. + * + * @param e the MessageCountEvent + */ + void messagesRemoved(MessageCountEvent e); +} diff --git a/net-mail/src/main/java/jakarta/mail/event/StoreEvent.java b/net-mail/src/main/java/jakarta/mail/event/StoreEvent.java new file mode 100644 index 0000000..b4a9941 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/StoreEvent.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +import jakarta.mail.Store; + +/** + * This class models notifications from the Store connection. These + * notifications can be ALERTS or NOTICES. ALERTS must be presented + * to the user in a fashion that calls the user's attention to the + * message. + * + * @author John Mani + */ +@SuppressWarnings("serial") +public class StoreEvent extends MailEvent { + + /** + * Indicates that this message is an ALERT. + */ + public static final int ALERT = 1; + + /** + * Indicates that this message is a NOTICE. + */ + public static final int NOTICE = 2; + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * The message text to be presented to the user. + * + * @serial + */ + protected String message; + + /** + * Construct a StoreEvent. + * + * @param store the source Store + * @param type the event type + * @param message a message assoicated with the event + */ + public StoreEvent(Store store, int type, String message) { + super(store); + this.type = type; + this.message = message; + } + + /** + * Return the type of this event. + * + * @return type + * @see #ALERT + * @see #NOTICE + */ + public int getMessageType() { + return type; + } + + /** + * Get the message from the Store. + * + * @return message from the Store + */ + public String getMessage() { + return message; + } + + /** + * Invokes the appropriate StoreListener method. + */ + @Override + public void dispatch(Object listener) { + ((StoreListener) listener).notification(this); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/event/StoreListener.java b/net-mail/src/main/java/jakarta/mail/event/StoreListener.java new file mode 100644 index 0000000..77c0692 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/StoreListener.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +/** + * This is the Listener interface for Store Notifications. + * + * @author John Mani + */ + +public interface StoreListener extends java.util.EventListener { + + /** + * Invoked when the Store generates a notification event. + * + * @param e the StoreEvent + * @see StoreEvent#ALERT + * @see StoreEvent#NOTICE + */ + void notification(StoreEvent e); +} diff --git a/net-mail/src/main/java/jakarta/mail/event/TransportAdapter.java b/net-mail/src/main/java/jakarta/mail/event/TransportAdapter.java new file mode 100644 index 0000000..e72817e --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/TransportAdapter.java @@ -0,0 +1,46 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +/** + * The adapter which receives Transport events. + * The methods in this class are empty; this class is provided as a + * convenience for easily creating listeners by extending this class + * and overriding only the methods of interest. + * + * @author John Mani + */ +public abstract class TransportAdapter implements TransportListener { + + /** + * Creates a default {@code TransportAdapter}. + */ + public TransportAdapter() { + } + + @Override + public void messageDelivered(TransportEvent e) { + } + + @Override + public void messageNotDelivered(TransportEvent e) { + } + + @Override + public void messagePartiallyDelivered(TransportEvent e) { + } +} diff --git a/net-mail/src/main/java/jakarta/mail/event/TransportEvent.java b/net-mail/src/main/java/jakarta/mail/event/TransportEvent.java new file mode 100644 index 0000000..6e687c4 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/TransportEvent.java @@ -0,0 +1,164 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +import jakarta.mail.Address; +import jakarta.mail.Message; +import jakarta.mail.Transport; + +/** + * This class models Transport events. + * + * @author John Mani + * @author Max Spivak + * @see Transport + * @see TransportListener + */ +@SuppressWarnings("serial") +public class TransportEvent extends MailEvent { + + /** + * Message has been successfully delivered to all recipients by the + * transport firing this event. validSent[] contains all the addresses + * this transport sent to successfully. validUnsent[] and invalid[] + * should be null, + */ + public static final int MESSAGE_DELIVERED = 1; + + /** + * Message was not sent for some reason. validSent[] should be null. + * validUnsent[] may have addresses that are valid (but the message + * wasn't sent to them). invalid[] should likely contain invalid addresses. + */ + public static final int MESSAGE_NOT_DELIVERED = 2; + + /** + * Message was successfully sent to some recipients but not to all. + * validSent[] holds addresses of recipients to whom the message was sent. + * validUnsent[] holds valid addresses to which the message was not sent. + * invalid[] holds invalid addresses, if any. + */ + public static final int MESSAGE_PARTIALLY_DELIVERED = 3; + + + /** + * The event type. + * + * @serial + */ + protected int type; + + /** + * The valid address to which the message was sent. + */ + transient protected Address[] validSent; + /** + * The valid address to which the message was not sent. + */ + transient protected Address[] validUnsent; + /** + * The invalid addresses. + */ + transient protected Address[] invalid; + /** + * The Message to which this event applies. + */ + transient protected Message msg; + + /** + * Constructor. + * + * @param transport The Transport object + * @param type the event type (MESSAGE_DELIVERED, etc.) + * @param validSent the valid addresses to which the message was sent + * @param validUnsent the valid addresses to which the message was + * not sent + * @param invalid the invalid addresses + * @param msg the message being sent + */ + public TransportEvent(Transport transport, int type, Address[] validSent, + Address[] validUnsent, Address[] invalid, + Message msg) { + super(transport); + this.type = type; + this.validSent = validSent; + this.validUnsent = validUnsent; + this.invalid = invalid; + this.msg = msg; + } + + /** + * Return the type of this event. + * + * @return type + */ + public int getType() { + return type; + } + + /** + * Return the addresses to which this message was sent succesfully. + * + * @return Addresses to which the message was sent successfully or null + */ + public Address[] getValidSentAddresses() { + return validSent; + } + + /** + * Return the addresses that are valid but to which this message + * was not sent. + * + * @return Addresses that are valid but to which the message was + * not sent successfully or null + */ + public Address[] getValidUnsentAddresses() { + return validUnsent; + } + + /** + * Return the addresses to which this message could not be sent. + * + * @return Addresses to which the message sending failed or null + */ + public Address[] getInvalidAddresses() { + return invalid; + } + + /** + * Get the Message object associated with this Transport Event. + * + * @return the Message object + * @since JavaMail 1.2 + */ + public Message getMessage() { + return msg; + } + + /** + * Invokes the appropriate TransportListener method. + */ + @Override + public void dispatch(Object listener) { + if (type == MESSAGE_DELIVERED) + ((TransportListener) listener).messageDelivered(this); + else if (type == MESSAGE_NOT_DELIVERED) + ((TransportListener) listener).messageNotDelivered(this); + else // MESSAGE_PARTIALLY_DELIVERED + ((TransportListener) listener).messagePartiallyDelivered(this); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/event/TransportListener.java b/net-mail/src/main/java/jakarta/mail/event/TransportListener.java new file mode 100644 index 0000000..847d58e --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/TransportListener.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.event; + +/** + * This is the Listener interface for Transport events + * + * @author John Mani + * @author Max Spivak + * @see jakarta.mail.Transport + * @see TransportEvent + */ + +public interface TransportListener extends java.util.EventListener { + + /** + * Invoked when a Message is succesfully delivered. + * + * @param e TransportEvent + */ + void messageDelivered(TransportEvent e); + + /** + * Invoked when a Message is not delivered. + * + * @param e TransportEvent + * @see TransportEvent + */ + void messageNotDelivered(TransportEvent e); + + /** + * Invoked when a Message is partially delivered. + * + * @param e TransportEvent + * @see TransportEvent + */ + void messagePartiallyDelivered(TransportEvent e); +} diff --git a/net-mail/src/main/java/jakarta/mail/event/package-info.java b/net-mail/src/main/java/jakarta/mail/event/package-info.java new file mode 100644 index 0000000..2ca6ac8 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/event/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * Listeners and events for the Jakarta Mail API. + * This package defines listener classes and event classes used by the classes + * defined in the jakarta.mail package. + */ +package jakarta.mail.event; \ No newline at end of file diff --git a/net-mail/src/main/java/jakarta/mail/internet/AddressException.java b/net-mail/src/main/java/jakarta/mail/internet/AddressException.java new file mode 100644 index 0000000..599ef6e --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/AddressException.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +/** + * The exception thrown when a wrongly formatted address is encountered. + * + * @author Bill Shannon + * @author Max Spivak + */ +@SuppressWarnings("serial") +public class AddressException extends ParseException { + /** + * The string being parsed. + * + * @serial + */ + protected String ref = null; + + /** + * The index in the string where the error occurred, or -1 if not known. + * + * @serial + */ + protected int pos = -1; + + /** + * Constructs an AddressException with no detail message. + */ + public AddressException() { + super(); + } + + /** + * Constructs an AddressException with the specified detail message. + * + * @param s the detail message + */ + public AddressException(String s) { + super(s); + } + + /** + * Constructs an AddressException with the specified detail message + * and reference info. + * + * @param s the detail message + * @param ref the string being parsed + */ + public AddressException(String s, String ref) { + super(s); + this.ref = ref; + } + + /** + * Constructs an AddressException with the specified detail message + * and reference info. + * + * @param s the detail message + * @param ref the string being parsed + * @param pos the position of the error + */ + public AddressException(String s, String ref, int pos) { + super(s); + this.ref = ref; + this.pos = pos; + } + + /** + * Get the string that was being parsed when the error was detected + * (null if not relevant). + * + * @return the string that was being parsed + */ + public String getRef() { + return ref; + } + + /** + * Get the position with the reference string where the error was + * detected (-1 if not relevant). + * + * @return the position within the string of the error + */ + public int getPos() { + return pos; + } + + @Override + public String toString() { + String s = super.toString(); + if (ref == null) + return s; + s += " in string ``" + ref + "''"; + if (pos < 0) + return s; + return s + " at position " + pos; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/ContentDisposition.java b/net-mail/src/main/java/jakarta/mail/internet/ContentDisposition.java new file mode 100644 index 0000000..6dc9034 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/ContentDisposition.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +/** + * This class represents a MIME ContentDisposition value. It provides + * methods to parse a ContentDisposition string into individual components + * and to generate a MIME style ContentDisposition string. + * + * @author John Mani + */ + +public class ContentDisposition { + + private static final boolean contentDispositionStrict = + MimeUtility.getBooleanSystemProperty("mail.mime.contentdisposition.strict", true); + + private String disposition; // disposition + private ParameterList list; // parameter list + + /** + * No-arg Constructor. + */ + public ContentDisposition() { + } + + /** + * Constructor. + * + * @param disposition disposition + * @param list ParameterList + * @since JavaMail 1.2 + */ + public ContentDisposition(String disposition, ParameterList list) { + this.disposition = disposition; + this.list = list; + } + + /** + * Constructor that takes a ContentDisposition string. The String + * is parsed into its constituents: dispostion and parameters. + * A ParseException is thrown if the parse fails. + * + * @param s the ContentDisposition string. + * @throws ParseException if the parse fails. + * @since JavaMail 1.2 + */ + public ContentDisposition(String s) throws ParseException { + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + HeaderTokenizer.Token tk; + + // First "disposition" .. + tk = h.next(); + if (tk.getType() != HeaderTokenizer.Token.ATOM) { + if (contentDispositionStrict) { + throw new ParseException("Expected disposition, got " + + tk.getValue()); + } + } else { + disposition = tk.getValue(); + } + + // Then parameters .. + String rem = h.getRemainder(); + if (rem != null) { + try { + list = new ParameterList(rem); + } catch (ParseException px) { + if (contentDispositionStrict) { + throw px; + } + } + } + } + + /** + * Return the disposition value. + * + * @return the disposition + * @since JavaMail 1.2 + */ + public String getDisposition() { + return disposition; + } + + /** + * Set the disposition. Replaces the existing disposition. + * + * @param disposition the disposition + * @since JavaMail 1.2 + */ + public void setDisposition(String disposition) { + this.disposition = disposition; + } + + /** + * Return the specified parameter value. Returns null + * if this parameter is absent. + * + * @param name the parameter name + * @return parameter value + * @since JavaMail 1.2 + */ + public String getParameter(String name) { + if (list == null) + return null; + + return list.get(name); + } + + /** + * Return a ParameterList object that holds all the available + * parameters. Returns null if no parameters are available. + * + * @return ParameterList + * @since JavaMail 1.2 + */ + public ParameterList getParameterList() { + return list; + } + + /** + * Set a new ParameterList. + * + * @param list ParameterList + * @since JavaMail 1.2 + */ + public void setParameterList(ParameterList list) { + this.list = list; + } + + /** + * Set the specified parameter. If this parameter already exists, + * it is replaced by this new value. + * + * @param name parameter name + * @param value parameter value + * @since JavaMail 1.2 + */ + public void setParameter(String name, String value) { + if (list == null) + list = new ParameterList(); + + list.set(name, value); + } + + /** + * Retrieve a RFC2045 style string representation of + * this ContentDisposition. Returns an empty string if + * the conversion failed. + * + * @return RFC2045 style string + * @since JavaMail 1.2 + */ + @Override + public String toString() { + if (disposition == null) + return ""; + + if (list == null) + return disposition; + + StringBuilder sb = new StringBuilder(disposition); + + // append the parameter list + // use the length of the string buffer + the length of + // the header name formatted as follows "Content-Disposition: " + sb.append(list.toString(sb.length() + 21)); + return sb.toString(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/ContentType.java b/net-mail/src/main/java/jakarta/mail/internet/ContentType.java new file mode 100644 index 0000000..cb4a467 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/ContentType.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +/** + * This class represents a MIME Content-Type value. It provides + * methods to parse a Content-Type string into individual components + * and to generate a MIME style Content-Type string. + * + * @author John Mani + */ + +public class ContentType { + + private String primaryType; // primary type + private String subType; // subtype + private ParameterList list; // parameter list + + /** + * No-arg Constructor. + */ + public ContentType() { + } + + /** + * Constructor. + * + * @param primaryType primary type + * @param subType subType + * @param list ParameterList + */ + public ContentType(String primaryType, String subType, + ParameterList list) { + this.primaryType = primaryType; + this.subType = subType; + this.list = list; + } + + /** + * Constructor that takes a Content-Type string. The String + * is parsed into its constituents: primaryType, subType + * and parameters. A ParseException is thrown if the parse fails. + * + * @param s the Content-Type string. + * @throws ParseException if the parse fails. + */ + public ContentType(String s) throws ParseException { + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + HeaderTokenizer.Token tk; + + // First "type" .. + tk = h.next(); + if (tk.getType() != HeaderTokenizer.Token.ATOM) + throw new ParseException("In Content-Type string <" + s + ">" + + ", expected MIME type, got " + + tk.getValue()); + primaryType = tk.getValue(); + + // The '/' separator .. + tk = h.next(); + if ((char) tk.getType() != '/') + throw new ParseException("In Content-Type string <" + s + ">" + + ", expected '/', got " + tk.getValue()); + + // Then "subType" .. + tk = h.next(); + if (tk.getType() != HeaderTokenizer.Token.ATOM) + throw new ParseException("In Content-Type string <" + s + ">" + + ", expected MIME subtype, got " + + tk.getValue()); + subType = tk.getValue(); + + // Finally parameters .. + String rem = h.getRemainder(); + if (rem != null) + list = new ParameterList(rem); + } + + /** + * Return the primary type. + * + * @return the primary type + */ + public String getPrimaryType() { + return primaryType; + } + + /** + * Set the primary type. Overrides existing primary type. + * + * @param primaryType primary type + */ + public void setPrimaryType(String primaryType) { + this.primaryType = primaryType; + } + + /** + * Return the subType. + * + * @return the subType + */ + public String getSubType() { + return subType; + } + + /** + * Set the subType. Replaces the existing subType. + * + * @param subType the subType + */ + public void setSubType(String subType) { + this.subType = subType; + } + + /** + * Return the MIME type string, without the parameters. + * The returned value is basically the concatenation of + * the primaryType, the '/' character and the secondaryType. + * + * @return the type + */ + public String getBaseType() { + if (primaryType == null || subType == null) + return ""; + return primaryType + '/' + subType; + } + + /** + * Return the specified parameter value. Returns null + * if this parameter is absent. + * + * @param name the parameter name + * @return parameter value + */ + public String getParameter(String name) { + if (list == null) + return null; + + return list.get(name); + } + + /** + * Return a ParameterList object that holds all the available + * parameters. Returns null if no parameters are available. + * + * @return ParameterList + */ + public ParameterList getParameterList() { + return list; + } + + /** + * Set a new ParameterList. + * + * @param list ParameterList + */ + public void setParameterList(ParameterList list) { + this.list = list; + } + + /** + * Set the specified parameter. If this parameter already exists, + * it is replaced by this new value. + * + * @param name parameter name + * @param value parameter value + */ + public void setParameter(String name, String value) { + if (list == null) + list = new ParameterList(); + + list.set(name, value); + } + + /** + * Retrieve a RFC2045 style string representation of + * this Content-Type. Returns an empty string if + * the conversion failed. + * + * @return RFC2045 style string + */ + @Override + public String toString() { + if (primaryType == null || subType == null) // need both + return ""; + + StringBuilder sb = new StringBuilder(); + sb.append(primaryType).append('/').append(subType); + if (list != null) + // append the parameter list + // use the length of the string buffer + the length of + // the header name formatted as follows "Content-Type: " + sb.append(list.toString(sb.length() + 14)); + + return sb.toString(); + } + + /** + * Match with the specified ContentType object. This method + * compares only the primaryType and + * subType . The parameters of both operands + * are ignored.

+ *

+ * For example, this method will return true when + * comparing the ContentTypes for "text/plain" + * and "text/plain; charset=foobar". + *

+ * If the subType of either operand is the special + * character '*', then the subtype is ignored during the match. + * For example, this method will return true when + * comparing the ContentTypes for "text/plain" + * and "text/*" + * + * @param cType ContentType to compare this against + * @return true if it matches + */ + public boolean match(ContentType cType) { + // Match primaryType + if (!((primaryType == null && cType.getPrimaryType() == null) || + (primaryType != null && + primaryType.equalsIgnoreCase(cType.getPrimaryType())))) + return false; + + String sType = cType.getSubType(); + + // If either one of the subTypes is wildcarded, return true + if ((subType != null && subType.startsWith("*")) || + (sType != null && sType.startsWith("*"))) + return true; + + // Match subType + return (subType == null && sType == null) || + (subType != null && subType.equalsIgnoreCase(sType)); + } + + /** + * Match with the specified content-type string. This method + * compares only the primaryType and + * subType . + * The parameters of both operands are ignored.

+ *

+ * For example, this method will return true when + * comparing the ContentType for "text/plain" + * with "text/plain; charset=foobar". + *

+ * If the subType of either operand is the special + * character '*', then the subtype is ignored during the match. + * For example, this method will return true when + * comparing the ContentType for "text/plain" + * with "text/*" + * + * @param s the content-type string to match + * @return true if it matches + */ + public boolean match(String s) { + try { + return match(new ContentType(s)); + } catch (ParseException pex) { + return false; + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/HeaderTokenizer.java b/net-mail/src/main/java/jakarta/mail/internet/HeaderTokenizer.java new file mode 100644 index 0000000..239bb2b --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/HeaderTokenizer.java @@ -0,0 +1,463 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +/** + * This class tokenizes RFC822 and MIME headers into the basic + * symbols specified by RFC822 and MIME.

+ *

+ * This class handles folded headers (ie headers with embedded + * CRLF SPACE sequences). The folds are removed in the returned + * tokens. + * + * @author John Mani + * @author Bill Shannon + */ + +public class HeaderTokenizer { + + /** + * RFC822 specials + */ + public final static String RFC822 = "()<>@,;:\\\"\t .[]"; + /** + * MIME specials + */ + public final static String MIME = "()<>@,;:\\\"\t []/?="; + // The EOF Token + private final static Token EOFToken = new Token(Token.EOF, null); + private String string; // the string to be tokenized + private boolean skipComments; // should comments be skipped ? + private String delimiters; // delimiter string + private int currentPos; // current parse position + private int maxPos; // string length + private int nextPos; // track start of next Token for next() + private int peekPos; // track start of next Token for peek() + + /** + * Constructor that takes a rfc822 style header. + * + * @param skipComments If true, comments are skipped and + * not returned as tokens + * @param header The rfc822 header to be tokenized + * @param delimiters Set of delimiter characters + * to be used to delimit ATOMS. These + * are usually RFC822 or + * MIME + */ + public HeaderTokenizer(String header, String delimiters, + boolean skipComments) { + string = (header == null) ? "" : header; // paranoia ?! + this.skipComments = skipComments; + this.delimiters = delimiters; + currentPos = nextPos = peekPos = 0; + maxPos = string.length(); + } + + /** + * Constructor. Comments are ignored and not returned as tokens + * + * @param header The header that is tokenized + * @param delimiters The delimiters to be used + */ + public HeaderTokenizer(String header, String delimiters) { + this(header, delimiters, true); + } + + /** + * Constructor. The RFC822 defined delimiters - RFC822 - are + * used to delimit ATOMS. Also comments are skipped and not + * returned as tokens + * + * @param header the header string + */ + public HeaderTokenizer(String header) { + this(header, RFC822); + } + + // Trim SPACE, HT, CR and NL from end of string + private static String trimWhiteSpace(String s) { + char c; + int i; + for (i = s.length() - 1; i >= 0; i--) { + if (((c = s.charAt(i)) != ' ') && + (c != '\t') && (c != '\r') && (c != '\n')) + break; + } + if (i <= 0) + return ""; + else + return s.substring(0, i + 1); + } + + /* Process escape sequences and embedded LWSPs from a comment or + * quoted string. + */ + private static String filterToken(String s, int start, int end, + boolean keepEscapes) { + StringBuilder sb = new StringBuilder(); + char c; + boolean gotEscape = false; + boolean gotCR = false; + + for (int i = start; i < end; i++) { + c = s.charAt(i); + if (c == '\n' && gotCR) { + // This LF is part of an unescaped + // CRLF sequence (i.e, LWSP). Skip it. + gotCR = false; + continue; + } + + gotCR = false; + if (!gotEscape) { + // Previous character was NOT '\' + if (c == '\\') // skip this character + gotEscape = true; + else if (c == '\r') // skip this character + gotCR = true; + else // append this character + sb.append(c); + } else { + // Previous character was '\'. So no need to + // bother with any special processing, just + // append this character. If keepEscapes is + // set, keep the backslash. IE6 fails to escape + // backslashes in quoted strings in HTTP headers, + // e.g., in the filename parameter. + if (keepEscapes) + sb.append('\\'); + sb.append(c); + gotEscape = false; + } + } + return sb.toString(); + } + + /** + * Parses the next token from this String.

+ *

+ * Clients sit in a loop calling next() to parse successive + * tokens until an EOF Token is returned. + * + * @return the next Token + * @throws ParseException if the parse fails + */ + public Token next() throws ParseException { + return next('\0', false); + } + + /** + * Parses the next token from this String. + * If endOfAtom is not NUL, the token extends until the + * endOfAtom character is seen, or to the end of the header. + * This method is useful when parsing headers that don't + * obey the MIME specification, e.g., by failing to quote + * parameter values that contain spaces. + * + * @param endOfAtom if not NUL, character marking end of token + * @return the next Token + * @throws ParseException if the parse fails + * @since JavaMail 1.5 + */ + public Token next(char endOfAtom) throws ParseException { + return next(endOfAtom, false); + } + + /** + * Parses the next token from this String. + * endOfAtom is handled as above. If keepEscapes is true, + * any backslash escapes are preserved in the returned string. + * This method is useful when parsing headers that don't + * obey the MIME specification, e.g., by failing to escape + * backslashes in the filename parameter. + * + * @param endOfAtom if not NUL, character marking end of token + * @param keepEscapes keep all backslashes in returned string? + * @return the next Token + * @throws ParseException if the parse fails + * @since JavaMail 1.5 + */ + public Token next(char endOfAtom, boolean keepEscapes) + throws ParseException { + Token tk; + + currentPos = nextPos; // setup currentPos + tk = getNext(endOfAtom, keepEscapes); + nextPos = peekPos = currentPos; // update currentPos and peekPos + return tk; + } + + /** + * Peek at the next token, without actually removing the token + * from the parse stream. Invoking this method multiple times + * will return successive tokens, until next() is + * called. + * + * @return the next Token + * @throws ParseException if the parse fails + */ + public Token peek() throws ParseException { + Token tk; + + currentPos = peekPos; // setup currentPos + tk = getNext('\0', false); + peekPos = currentPos; // update peekPos + return tk; + } + + /** + * Return the rest of the Header. + * + * @return String rest of header. null is returned if we are + * already at end of header + */ + public String getRemainder() { + if (nextPos >= string.length()) + return null; + return string.substring(nextPos); + } + + /* + * Return the next token starting from 'currentPos'. After the + * parse, 'currentPos' is updated to point to the start of the + * next token. + */ + private Token getNext(char endOfAtom, boolean keepEscapes) + throws ParseException { + // If we're already at end of string, return EOF + if (currentPos >= maxPos) + return EOFToken; + + // Skip white-space, position currentPos beyond the space + if (skipWhiteSpace() == Token.EOF) + return EOFToken; + + char c; + int start; + boolean filter = false; + + c = string.charAt(currentPos); + + // Check or Skip comments and position currentPos + // beyond the comment + while (c == '(') { + // Parsing comment .. + int nesting; + for (start = ++currentPos, nesting = 1; + nesting > 0 && currentPos < maxPos; + currentPos++) { + c = string.charAt(currentPos); + if (c == '\\') { // Escape sequence + currentPos++; // skip the escaped character + filter = true; + } else if (c == '\r') + filter = true; + else if (c == '(') + nesting++; + else if (c == ')') + nesting--; + } + if (nesting != 0) + throw new ParseException("Unbalanced comments"); + + if (!skipComments) { + // Return the comment, if we are asked to. + // Note that the comment start & end markers are ignored. + String s; + if (filter) // need to go thru the token again. + s = filterToken(string, start, currentPos - 1, keepEscapes); + else + s = string.substring(start, currentPos - 1); + + return new Token(Token.COMMENT, s); + } + + // Skip any whitespace after the comment. + if (skipWhiteSpace() == Token.EOF) + return EOFToken; + c = string.charAt(currentPos); + } + + // Check for quoted-string and position currentPos + // beyond the terminating quote + if (c == '"') { + currentPos++; // skip initial quote + return collectString('"', keepEscapes); + } + + // Check for SPECIAL or CTL + if (c < 040 || c >= 0177 || delimiters.indexOf(c) >= 0) { + if (endOfAtom > 0 && c != endOfAtom) { + // not expecting a special character here, + // pretend it's a quoted string + return collectString(endOfAtom, keepEscapes); + } + currentPos++; // re-position currentPos + char[] ch = new char[1]; + ch[0] = c; + return new Token(c, new String(ch)); + } + + // Check for ATOM + for (start = currentPos; currentPos < maxPos; currentPos++) { + c = string.charAt(currentPos); + // ATOM is delimited by either SPACE, CTL, "(", <"> + // or the specified SPECIALS + if (c < 040 || c >= 0177 || c == '(' || c == ' ' || + c == '"' || delimiters.indexOf(c) >= 0) { + if (endOfAtom > 0 && c != endOfAtom) { + // not the expected atom after all; + // back up and pretend it's a quoted string + currentPos = start; + return collectString(endOfAtom, keepEscapes); + } + break; + } + } + return new Token(Token.ATOM, string.substring(start, currentPos)); + } + + private Token collectString(char eos, boolean keepEscapes) + throws ParseException { + int start; + boolean filter = false; + for (start = currentPos; currentPos < maxPos; currentPos++) { + char c = string.charAt(currentPos); + if (c == '\\') { // Escape sequence + currentPos++; + filter = true; + } else if (c == '\r') + filter = true; + else if (c == eos) { + currentPos++; + String s; + + if (filter) + s = filterToken(string, start, currentPos - 1, keepEscapes); + else + s = string.substring(start, currentPos - 1); + + if (c != '"') { // not a real quoted string + s = trimWhiteSpace(s); + currentPos--; // back up before the eos char + } + + return new Token(Token.QUOTEDSTRING, s); + } + } + + // ran off the end of the string + + // if we're looking for a matching quote, that's an error + if (eos == '"') + throw new ParseException("Unbalanced quoted string"); + + // otherwise, just return whatever's left + String s; + if (filter) + s = filterToken(string, start, currentPos, keepEscapes); + else + s = string.substring(start, currentPos); + s = trimWhiteSpace(s); + return new Token(Token.QUOTEDSTRING, s); + } + + // Skip SPACE, HT, CR and NL + private int skipWhiteSpace() { + char c; + for (; currentPos < maxPos; currentPos++) + if (((c = string.charAt(currentPos)) != ' ') && + (c != '\t') && (c != '\r') && (c != '\n')) + return currentPos; + return Token.EOF; + } + + /** + * The Token class represents tokens returned by the + * HeaderTokenizer. + */ + public static class Token { + + /** + * Token type indicating an ATOM. + */ + public static final int ATOM = -1; + /** + * Token type indicating a quoted string. The value + * field contains the string without the quotes. + */ + public static final int QUOTEDSTRING = -2; + /** + * Token type indicating a comment. The value field + * contains the comment string without the comment + * start and end symbols. + */ + public static final int COMMENT = -3; + /** + * Token type indicating end of input. + */ + public static final int EOF = -4; + private int type; + private String value; + + /** + * Constructor. + * + * @param type Token type + * @param value Token value + */ + public Token(int type, String value) { + this.type = type; + this.value = value; + } + + /** + * Return the type of the token. If the token represents a + * delimiter or a control character, the type is that character + * itself, converted to an integer. Otherwise, it's value is + * one of the following: + *

    + *
  • ATOM A sequence of ASCII characters + * delimited by either SPACE, CTL, "(", <"> or the + * specified SPECIALS + *
  • QUOTEDSTRING A sequence of ASCII characters + * within quotes + *
  • COMMENT A sequence of ASCII characters + * within "(" and ")". + *
  • EOF End of header + *
+ * + * @return the token type + */ + public int getType() { + return type; + } + + /** + * Returns the value of the token just read. When the current + * token is a quoted string, this field contains the body of the + * string, without the quotes. When the current token is a comment, + * this field contains the body of the comment. + * + * @return token value + */ + public String getValue() { + return value; + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/InternetAddress.java b/net-mail/src/main/java/jakarta/mail/internet/InternetAddress.java new file mode 100644 index 0000000..3e2c37d --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/InternetAddress.java @@ -0,0 +1,1508 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import jakarta.mail.Address; +import jakarta.mail.Session; +import java.io.UnsupportedEncodingException; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.StringTokenizer; + +/** + * This class represents an Internet email address using the syntax + * of RFC822. + * Typical address syntax is of the form "user@host.domain" or + * "Personal Name <user@host.domain>". + * + * @author Bill Shannon + * @author John Mani + */ +@SuppressWarnings("serial") +public class InternetAddress extends Address { + + private static final boolean ignoreBogusGroupName = + MimeUtility.getBooleanSystemProperty( + "mail.mime.address.ignorebogusgroupname", true); + private static final boolean useCanonicalHostName = + MimeUtility.getBooleanSystemProperty( + "mail.mime.address.usecanonicalhostname", true); + private static final boolean allowUtf8 = + MimeUtility.getBooleanSystemProperty("mail.mime.allowutf8", false); + private static final String rfc822phrase = + HeaderTokenizer.RFC822.replace(' ', '\0').replace('\t', '\0'); + private static final String specialsNoDotNoAt = "()<>,;:\\\"[]"; + private static final String specialsNoDot = specialsNoDotNoAt + "@"; + /** + * The email address. + */ + protected String address; + /** + * The personal name. + */ + protected String personal; + /** + * The RFC 2047 encoded version of the personal name.

+ *

+ * This field and the personal field track each + * other, so if a subclass sets one of these fields directly, it + * should set the other to null, so that it is + * suitably recomputed. + */ + protected String encodedPersonal; + + /** + * Default constructor. + */ + public InternetAddress() { + } + + /** + * Constructor.

+ *

+ * Parse the given string and create an InternetAddress. + * See the parse method for details of the parsing. + * The address is parsed using "strict" parsing. + * This constructor does not perform the additional + * syntax checks that the + * InternetAddress(String address, boolean strict) + * constructor does when strict is true. + * This constructor is equivalent to + * InternetAddress(address, false). + * + * @param address the address in RFC822 format + * @throws AddressException if the parse failed + */ + public InternetAddress(String address) throws AddressException { + // use our address parsing utility routine to parse the string + InternetAddress[] a = parse(address, true); + // if we got back anything other than a single address, it's an error + if (a.length != 1) { + throw new AddressException("Illegal address", address); + } + /* + * Now copy the contents of the single address we parsed + * into the current object, which will be returned from the + * constructor. + * XXX - this sure is a round-about way of getting this done. + */ + this.address = a[0].address; + this.personal = a[0].personal; + this.encodedPersonal = a[0].encodedPersonal; + } + + /** + * Parse the given string and create an InternetAddress. + * If strict is false, the detailed syntax of the + * address isn't checked. + * + * @param address the address in RFC822 format + * @param strict enforce RFC822 syntax + * @throws AddressException if the parse failed + * @since JavaMail 1.3 + */ + @SuppressWarnings("this-escape") + public InternetAddress(String address, boolean strict) + throws AddressException { + this(address); + if (strict) { + if (isGroup()) { + getGroup(true); // throw away the result + } else { + checkAddress(this.address, true, true); + } + } + } + + /** + * Construct an InternetAddress given the address and personal name. + * The address is assumed to be a syntactically valid RFC822 address. + * + * @param address the address in RFC822 format + * @param personal the personal name + * @throws UnsupportedEncodingException if the personal name + * can't be encoded in the given charset + */ + public InternetAddress(String address, String personal) + throws UnsupportedEncodingException { + this(address, personal, null); + } + + /** + * Construct an InternetAddress given the address and personal name. + * The address is assumed to be a syntactically valid RFC822 address. + * + * @param address the address in RFC822 format + * @param personal the personal name + * @param charset the MIME charset for the name + * @throws UnsupportedEncodingException if the personal name + * can't be encoded in the given charset + */ + public InternetAddress(String address, String personal, String charset) + throws UnsupportedEncodingException { + this.address = address; + setPersonal(personal, charset); + } + + private static String quotePhrase(String phrase) { + int len = phrase.length(); + boolean needQuoting = false; + + for (int i = 0; i < len; i++) { + char c = phrase.charAt(i); + if (c == '"' || c == '\\') { + // need to escape them and then quote the whole string + StringBuilder sb = new StringBuilder(len + 3); + sb.append('"'); + for (int j = 0; j < len; j++) { + char cc = phrase.charAt(j); + if (cc == '"' || cc == '\\') + // Escape the character + sb.append('\\'); + sb.append(cc); + } + sb.append('"'); + return sb.toString(); + } else if ((c < 040 && c != '\r' && c != '\n' && c != '\t') || + (c >= 0177 && !allowUtf8) || rfc822phrase.indexOf(c) >= 0) + // These characters cause the string to be quoted + needQuoting = true; + } + + if (needQuoting) { + StringBuilder sb = new StringBuilder(len + 2); + sb.append('"').append(phrase).append('"'); + return sb.toString(); + } else + return phrase; + } + + private static String unquote(String s) { + if (s.startsWith("\"") && s.endsWith("\"") && s.length() > 1) { + s = s.substring(1, s.length() - 1); + // check for any escaped characters + if (s.indexOf('\\') >= 0) { + StringBuilder sb = new StringBuilder(s.length()); // approx + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && i < s.length() - 1) + c = s.charAt(++i); + sb.append(c); + } + s = sb.toString(); + } + } + return s; + } + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains only US-ASCII characters, and + * hence is mail-safe. + * + * @param addresses array of InternetAddress objects + * @return comma separated string of addresses + * @throws ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + */ + public static String toString(Address[] addresses) { + return toString(addresses, 0); + } + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains Unicode characters. + * + * @param addresses array of InternetAddress objects + * @return comma separated string of addresses + * @throws ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + * @since JavaMail 1.6 + */ + public static String toUnicodeString(Address[] addresses) { + return toUnicodeString(addresses, 0); + } + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains only US-ASCII characters, and + * hence is mail-safe.

+ *

+ * The 'used' parameter specifies the number of character positions + * already taken up in the field into which the resulting address + * sequence string is to be inserted. It is used to determine the + * line-break positions in the resulting address sequence string. + * + * @param addresses array of InternetAddress objects + * @param used number of character positions already used, in + * the field into which the address string is to + * be inserted. + * @return comma separated string of addresses + * @throws ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + */ + public static String toString(Address[] addresses, int used) { + if (addresses == null || addresses.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + + for (int i = 0; i < addresses.length; i++) { + if (i != 0) { // need to append comma + sb.append(", "); + used += 2; + } + + // prefer not to split a single address across lines so used=0 below + String s = MimeUtility.fold(0, addresses[i].toString()); + int len = lengthOfFirstSegment(s); // length till CRLF + if (used + len > 76) { // overflows ... + // smash trailing space from ", " above + int curlen = sb.length(); + if (curlen > 0 && sb.charAt(curlen - 1) == ' ') + sb.setLength(curlen - 1); + sb.append("\r\n\t"); // .. start new continuation line + used = 8; // account for the starting char + } + sb.append(s); + used = lengthOfLastSegment(s, used); + } + + return sb.toString(); + } + + /* + * quotePhrase() quotes the words within a RFC822 phrase. + * + * This is tricky, since a phrase is defined as 1 or more + * RFC822 words, separated by LWSP. Now, a word that contains + * LWSP is supposed to be quoted, and this is exactly what the + * MimeUtility.quote() method does. However, when dealing with + * a phrase, any LWSP encountered can be construed to be the + * separator between words, and not part of the words themselves. + * To deal with this funkiness, we have the below variant of + * MimeUtility.quote(), which essentially ignores LWSP when + * deciding whether to quote a word. + * + * It aint pretty, but it gets the job done :) + */ + + /** + * Convert the given array of InternetAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains Unicode characters.

+ *

+ * The 'used' parameter specifies the number of character positions + * already taken up in the field into which the resulting address + * sequence string is to be inserted. It is used to determine the + * line-break positions in the resulting address sequence string. + * + * @param addresses array of InternetAddress objects + * @param used number of character positions already used, in + * the field into which the address string is to + * be inserted. + * @return comma separated string of addresses + * @throws ClassCastException if any address object in the + * given array is not an InternetAddress object. Note + * that this is a RuntimeException. + * @since JavaMail 1.6 + */ + /* + * XXX - This is exactly the same as the above, except it uses + * toUnicodeString instead of toString. + * XXX - Since the line length restrictions are in bytes, not characters, + * we convert all non-ASCII addresses to UTF-8 byte strings, + * which we then convert to ISO-8859-1 Strings where every + * character respresents one UTF-8 byte. At the end we reverse + * the conversion to get back to a correct Unicode string. + * This is a hack to allow all the other character-based methods + * to work properly with UTF-8 bytes. + */ + public static String toUnicodeString(Address[] addresses, int used) { + if (addresses == null || addresses.length == 0) + return null; + + StringBuilder sb = new StringBuilder(); + + boolean sawNonAscii = false; + for (int i = 0; i < addresses.length; i++) { + if (i != 0) { // need to append comma + sb.append(", "); + used += 2; + } + + // prefer not to split a single address across lines so used=0 below + String as = ((InternetAddress) addresses[i]).toUnicodeString(); + if (MimeUtility.checkAscii(as) != MimeUtility.ALL_ASCII) { + sawNonAscii = true; + as = new String(as.getBytes(StandardCharsets.UTF_8), + StandardCharsets.ISO_8859_1); + } + String s = MimeUtility.fold(0, as); + int len = lengthOfFirstSegment(s); // length till CRLF + if (used + len > 76) { // overflows ... + // smash trailing space from ", " above + int curlen = sb.length(); + if (curlen > 0 && sb.charAt(curlen - 1) == ' ') + sb.setLength(curlen - 1); + sb.append("\r\n\t"); // .. start new continuation line + used = 8; // account for the starting char + } + sb.append(s); + used = lengthOfLastSegment(s, used); + } + + String ret = sb.toString(); + if (sawNonAscii) + ret = new String(ret.getBytes(StandardCharsets.ISO_8859_1), + StandardCharsets.UTF_8); + return ret; + } + + /* + * Return the length of the first segment within this string. + * If no segments exist, the length of the whole line is returned. + */ + private static int lengthOfFirstSegment(String s) { + int pos; + if ((pos = s.indexOf("\r\n")) != -1) + return pos; + else + return s.length(); + } + + /* + * Return the length of the last segment within this string. + * If no segments exist, the length of the whole line plus + * used is returned. + */ + private static int lengthOfLastSegment(String s, int used) { + int pos; + if ((pos = s.lastIndexOf("\r\n")) != -1) + return s.length() - pos - 2; + else + return s.length() + used; + } + + /** + * Return an InternetAddress object representing the current user. + * The entire email address may be specified in the "mail.from" + * property. If not set, the "mail.user" and "mail.host" properties + * are tried. If those are not set, the "user.name" property and + * InetAddress.getLocalHost method are tried. + * If it is not possible to determine an email address, + * null is returned. + * + * @param session Session object used for property lookup + * @return current user's email address + */ + public static InternetAddress getLocalAddress(Session session) { + try { + return _getLocalAddress(session); + } catch (AddressException | UnknownHostException sex) { // ignore it + } // ignore it + return null; + } + + /** + * A package-private version of getLocalAddress that doesn't swallow + * the exception. Used by MimeMessage.setFrom() to report the reason + * for the failure. + */ + // package-private + static InternetAddress _getLocalAddress(Session session) + throws AddressException, UnknownHostException { + String user = null, host = null, address = null; + if (session == null) { + user = System.getProperty("user.name"); + host = getLocalHostName(); + } else { + address = session.getProperty("mail.from"); + if (address == null) { + user = session.getProperty("mail.user"); + if (user == null || user.isEmpty()) + user = session.getProperty("user.name"); + if (user == null || user.isEmpty()) + user = System.getProperty("user.name"); + host = session.getProperty("mail.host"); + if (host == null || host.isEmpty()) + host = getLocalHostName(); + } + } + + if (address == null && user != null && user.length() != 0 && + host != null && host.length() != 0) + address = MimeUtility.quote(user.trim(), specialsNoDot + "\t ") + + "@" + host; + + if (address == null) + return null; + + return new InternetAddress(address); + } + + /** + * Get the local host name from InetAddress and return it in a form + * suitable for use in an email address. + */ + private static String getLocalHostName() throws UnknownHostException { + String host = null; + InetAddress me = InetAddress.getLocalHost(); + if (me != null) { + // try canonical host name first + if (useCanonicalHostName) + host = me.getCanonicalHostName(); + if (host == null) + host = me.getHostName(); + // if we can't get our name, use local address literal + if (host == null) + host = me.getHostAddress(); + if (host != null && host.length() > 0 && isInetAddressLiteral(host)) + host = '[' + host + ']'; + } + return host; + } + + /** + * Is the address an IPv4 or IPv6 address literal, which needs to + * be enclosed in "[]" in an email address? IPv4 literals contain + * decimal digits and dots, IPv6 literals contain hex digits, dots, + * and colons. We're lazy and don't check the exact syntax, just + * the allowed characters; strings that have only the allowed + * characters in a literal but don't meet the syntax requirements + * for a literal definitely can't be a host name and thus will fail + * later when used as an address literal. + */ + private static boolean isInetAddressLiteral(String addr) { + boolean sawHex = false, sawColon = false; + for (int i = 0; i < addr.length(); i++) { + char c = addr.charAt(i); + if (c >= '0' && c <= '9') + ; // digits always ok + else if (c == '.') + ; // dot always ok + else if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) + sawHex = true; // need to see a colon too + else if (c == ':') + sawColon = true; + else + return false; // anything else, definitely not a literal + } + return !sawHex || sawColon; + } + + /** + * Parse the given comma separated sequence of addresses into + * InternetAddress objects. Addresses must follow RFC822 syntax. + * + * @param addresslist comma separated address strings + * @return array of InternetAddress objects + * @throws AddressException if the parse failed + */ + public static InternetAddress[] parse(String addresslist) + throws AddressException { + return parse(addresslist, true); + } + + /** + * Parse the given sequence of addresses into InternetAddress + * objects. If strict is false, simple email addresses + * separated by spaces are also allowed. If strict is + * true, many (but not all) of the RFC822 syntax rules are enforced. + * In particular, even if strict is true, addresses + * composed of simple names (with no "@domain" part) are allowed. + * Such "illegal" addresses are not uncommon in real messages.

+ *

+ * Non-strict parsing is typically used when parsing a list of + * mail addresses entered by a human. Strict parsing is typically + * used when parsing address headers in mail messages. + * + * @param addresslist comma separated address strings + * @param strict enforce RFC822 syntax + * @return array of InternetAddress objects + * @throws AddressException if the parse failed + */ + public static InternetAddress[] parse(String addresslist, boolean strict) + throws AddressException { + return parse(addresslist, strict, false); + } + + /** + * Parse the given sequence of addresses into InternetAddress + * objects. If strict is false, the full syntax rules for + * individual addresses are not enforced. If strict is + * true, many (but not all) of the RFC822 syntax rules are enforced.

+ *

+ * To better support the range of "invalid" addresses seen in real + * messages, this method enforces fewer syntax rules than the + * parse method when the strict flag is false + * and enforces more rules when the strict flag is true. If the + * strict flag is false and the parse is successful in separating out an + * email address or addresses, the syntax of the addresses themselves + * is not checked. + * + * @param addresslist comma separated address strings + * @param strict enforce RFC822 syntax + * @return array of InternetAddress objects + * @throws AddressException if the parse failed + * @since JavaMail 1.3 + */ + public static InternetAddress[] parseHeader(String addresslist, + boolean strict) throws AddressException { + return parse(MimeUtility.unfold(addresslist), strict, true); + } + + /* + * RFC822 Address parser. + * + * XXX - This is complex enough that it ought to be a real parser, + * not this ad-hoc mess, and because of that, this is not perfect. + * + * XXX - Deal with encoded Headers too. + */ + @SuppressWarnings("fallthrough") + private static InternetAddress[] parse(String s, boolean strict, + boolean parseHdr) throws AddressException { + int start, end, index, nesting; + int start_personal = -1, end_personal = -1; + int length = s.length(); + boolean ignoreErrors = parseHdr && !strict; + boolean in_group = false; // we're processing a group term + boolean route_addr = false; // address came from route-addr term + boolean rfc822 = false; // looks like an RFC822 address + char c; + List v = new ArrayList<>(); + InternetAddress ma; + + for (start = end = -1, index = 0; index < length; index++) { + c = s.charAt(index); + + switch (c) { + case '(': // We are parsing a Comment. Ignore everything inside. + // XXX - comment fields should be parsed as whitespace, + // more than one allowed per address + rfc822 = true; + if (start >= 0 && end == -1) + end = index; + int pindex = index; + for (index++, nesting = 1; index < length && nesting > 0; + index++) { + c = s.charAt(index); + switch (c) { + case '\\': + index++; // skip both '\' and the escaped char + break; + case '(': + nesting++; + break; + case ')': + nesting--; + break; + default: + break; + } + } + if (nesting > 0) { + if (!ignoreErrors) + throw new AddressException("Missing ')'", s, index); + // pretend the first paren was a regular character and + // continue parsing after it + index = pindex + 1; + break; + } + index--; // point to closing paren + if (start_personal == -1) + start_personal = pindex + 1; + if (end_personal == -1) + end_personal = index; + break; + + case ')': + if (!ignoreErrors) + throw new AddressException("Missing '('", s, index); + // pretend the left paren was a regular character and + // continue parsing + if (start == -1) + start = index; + break; + + case '<': + rfc822 = true; + if (route_addr) { + if (!ignoreErrors) + throw new AddressException( + "Extra route-addr", s, index); + + // assume missing comma between addresses + if (start == -1) { + route_addr = false; + rfc822 = false; + start = end = -1; + break; // nope, nothing there + } + if (!in_group) { + // got a token, add this to our InternetAddress list + if (end == -1) // should never happen + end = index; + String addr = s.substring(start, end).trim(); + + ma = new InternetAddress(); + ma.setAddress(addr); + if (start_personal >= 0) { + ma.encodedPersonal = unquote( + s.substring(start_personal, end_personal). + trim()); + } + v.add(ma); + + route_addr = false; + rfc822 = false; + start = end = -1; + start_personal = end_personal = -1; + // continue processing this new address... + } + } + + int rindex = index; + boolean inquote = false; + outf: + for (index++; index < length; index++) { + c = s.charAt(index); + switch (c) { + case '\\': // XXX - is this needed? + index++; // skip both '\' and the escaped char + break; + case '"': + inquote = !inquote; + break; + case '>': + if (inquote) + continue; + break outf; // out of for loop + default: + break; + } + } + + // did we find a matching quote? + if (inquote) { + if (!ignoreErrors) + throw new AddressException("Missing '\"'", s, index); + // didn't find matching quote, try again ignoring quotes + // (e.g., ``<"@foo.com>'') + outq: + for (index = rindex + 1; index < length; index++) { + c = s.charAt(index); + if (c == '\\') // XXX - is this needed? + index++; // skip both '\' and the escaped char + else if (c == '>') + break; + } + } + + // did we find a terminating '>'? + if (index >= length) { + if (!ignoreErrors) + throw new AddressException("Missing '>'", s, index); + // pretend the "<" was a regular character and + // continue parsing after it (e.g., ``<@foo.com'') + index = rindex + 1; + if (start == -1) + start = rindex; // back up to include "<" + break; + } + + if (!in_group) { + if (start >= 0) { + // seen some characters? use them as the personal name + start_personal = start; + end_personal = rindex; + } + start = rindex + 1; + } + route_addr = true; + end = index; + break; + + case '>': + if (!ignoreErrors) + throw new AddressException("Missing '<'", s, index); + // pretend the ">" was a regular character and + // continue parsing (e.g., ``>@foo.com'') + if (start == -1) + start = index; + break; + + case '"': // parse quoted string + int qindex = index; + rfc822 = true; + if (start == -1) + start = index; + outq: + for (index++; index < length; index++) { + c = s.charAt(index); + switch (c) { + case '\\': + index++; // skip both '\' and the escaped char + break; + case '"': + break outq; // out of for loop + default: + break; + } + } + if (index >= length) { + if (!ignoreErrors) + throw new AddressException("Missing '\"'", s, index); + // pretend the quote was a regular character and + // continue parsing after it (e.g., ``"@foo.com'') + index = qindex + 1; + } + break; + + case '[': // a domain-literal, probably + int lindex = index; + rfc822 = true; + if (start == -1) + start = index; + outb: + for (index++; index < length; index++) { + c = s.charAt(index); + switch (c) { + case '\\': + index++; // skip both '\' and the escaped char + break; + case ']': + break outb; // out of for loop + default: + break; + } + } + if (index >= length) { + if (!ignoreErrors) + throw new AddressException("Missing ']'", s, index); + // pretend the "[" was a regular character and + // continue parsing after it (e.g., ``[@foo.com'') + index = lindex + 1; + } + break; + + case ';': + if (start == -1) { + route_addr = false; + rfc822 = false; + start = end = -1; + break; // nope, nothing there + } + if (in_group) { + in_group = false; + /* + * If parsing headers, but not strictly, peek ahead. + * If next char is "@", treat the group name + * like the local part of the address, e.g., + * "Undisclosed-Recipient:;@java.sun.com". + */ + if (parseHdr && !strict && + index + 1 < length && s.charAt(index + 1) == '@') + break; + ma = new InternetAddress(); + end = index + 1; + ma.setAddress(s.substring(start, end).trim()); + v.add(ma); + + route_addr = false; + rfc822 = false; + start = end = -1; + start_personal = end_personal = -1; + break; + } + if (!ignoreErrors) + throw new AddressException( + "Illegal semicolon, not in group", s, index); + + // otherwise, parsing a header; treat semicolon like comma + // fall through to comma case... + + case ',': // end of an address, probably + if (start == -1) { + route_addr = false; + rfc822 = false; + start = end = -1; + break; // nope, nothing there + } + if (in_group) { + route_addr = false; + break; + } + // got a token, add this to our InternetAddress list + if (end == -1) + end = index; + + String addr = s.substring(start, end).trim(); + String pers = null; + if (rfc822 && start_personal >= 0) { + pers = unquote( + s.substring(start_personal, end_personal).trim()); + if (pers.trim().isEmpty()) + pers = null; + } + + /* + * If the personal name field has an "@" and the address + * field does not, assume they were reversed, e.g., + * ``"joe doe" (john.doe@example.com)''. + */ + if (parseHdr && !strict && pers != null && + pers.indexOf('@') >= 0 && + addr.indexOf('@') < 0 && addr.indexOf('!') < 0) { + String tmp = addr; + addr = pers; + pers = tmp; + } + if (rfc822 || strict || parseHdr) { + if (!ignoreErrors) + checkAddress(addr, route_addr, false); + ma = new InternetAddress(); + ma.setAddress(addr); + if (pers != null) + ma.encodedPersonal = pers; + v.add(ma); + } else { + // maybe we passed over more than one space-separated addr + StringTokenizer st = new StringTokenizer(addr); + while (st.hasMoreTokens()) { + String a = st.nextToken(); + checkAddress(a, false, false); + ma = new InternetAddress(); + ma.setAddress(a); + v.add(ma); + } + } + + route_addr = false; + rfc822 = false; + start = end = -1; + start_personal = end_personal = -1; + break; + + case ':': + rfc822 = true; + if (in_group) + if (!ignoreErrors) + throw new AddressException("Nested group", s, index); + if (start == -1) + start = index; + if (parseHdr && !strict) { + /* + * If next char is a special character that can't occur at + * the start of a valid address, treat the group name + * as the entire address, e.g., "Date:, Tue", "Re:@foo". + */ + if (index + 1 < length) { + String addressSpecials = ")>[]:@\\,."; + char nc = s.charAt(index + 1); + if (addressSpecials.indexOf(nc) >= 0) { + if (nc != '@') + break; // don't change in_group + /* + * Handle a common error: + * ``Undisclosed-Recipient:@example.com;'' + * + * Scan ahead. If we find a semicolon before + * one of these other special characters, + * consider it to be a group after all. + */ + for (int i = index + 2; i < length; i++) { + nc = s.charAt(i); + if (nc == ';') + break; + if (addressSpecials.indexOf(nc) >= 0) + break; + } + if (nc == ';') + break; // don't change in_group + } + } + + // ignore bogus "mailto:" prefix in front of an address, + // or bogus mail header name included in the address field + String gname = s.substring(start, index); + if (ignoreBogusGroupName && + (gname.equalsIgnoreCase("mailto") || + gname.equalsIgnoreCase("From") || + gname.equalsIgnoreCase("To") || + gname.equalsIgnoreCase("Cc") || + gname.equalsIgnoreCase("Subject") || + gname.equalsIgnoreCase("Re"))) + start = -1; // we're not really in a group + else + in_group = true; + } else + in_group = true; + break; + + // Ignore whitespace + case ' ': + case '\t': + case '\r': + case '\n': + break; + + default: + if (start == -1) + start = index; + break; + } + } + + if (start >= 0) { + /* + * The last token, add this to our InternetAddress list. + * Note that this block of code should be identical to the + * block above for "case ','". + */ + if (end == -1) + end = length; + + String addr = s.substring(start, end).trim(); + String pers = null; + if (rfc822 && start_personal >= 0) { + pers = unquote( + s.substring(start_personal, end_personal).trim()); + if (pers.trim().isEmpty()) + pers = null; + } + + /* + * If the personal name field has an "@" and the address + * field does not, assume they were reversed, e.g., + * ``"joe doe" (john.doe@example.com)''. + */ + if (parseHdr && !strict && + pers != null && pers.indexOf('@') >= 0 && + addr.indexOf('@') < 0 && addr.indexOf('!') < 0) { + String tmp = addr; + addr = pers; + pers = tmp; + } + if (rfc822 || strict || parseHdr) { + if (!ignoreErrors) + checkAddress(addr, route_addr, false); + ma = new InternetAddress(); + ma.setAddress(addr); + if (pers != null) + ma.encodedPersonal = pers; + v.add(ma); + } else { + // maybe we passed over more than one space-separated addr + StringTokenizer st = new StringTokenizer(addr); + while (st.hasMoreTokens()) { + String a = st.nextToken(); + checkAddress(a, false, false); + ma = new InternetAddress(); + ma.setAddress(a); + v.add(ma); + } + } + } + + InternetAddress[] a = new InternetAddress[v.size()]; + v.toArray(a); + return a; + } + + /** + * Check that the address is a valid "mailbox" per RFC822. + * (We also allow simple names.) + *

+ * XXX - much more to check + * XXX - doesn't handle domain-literals properly (but no one uses them) + */ + private static void checkAddress(String addr, + boolean routeAddr, boolean validate) + throws AddressException { + int i, start = 0; + + if (addr == null) + throw new AddressException("Address is null"); + int len = addr.length(); + if (len == 0) + throw new AddressException("Empty address", addr); + + /* + * routeAddr indicates that the address is allowed + * to have an RFC 822 "route". + */ + if (routeAddr && addr.charAt(0) == '@') { + /* + * Check for a legal "route-addr": + * [@domain[,@domain ...]:]local@domain + */ + for (start = 0; (i = indexOfAny(addr, ",:", start)) >= 0; + start = i + 1) { + if (addr.charAt(start) != '@') + throw new AddressException("Illegal route-addr", addr); + if (addr.charAt(i) == ':') { + // end of route-addr + start = i + 1; + break; + } + } + } + + /* + * The rest should be "local@domain", but we allow simply "local" + * unless called from validate. + * + * local-part must follow RFC 822 - no specials except '.' + * unless quoted. + */ + + char c = (char) -1; + char lastc = (char) -1; + boolean inquote = false; + for (i = start; i < len; i++) { + lastc = c; + c = addr.charAt(i); + // a quoted-pair is only supposed to occur inside a quoted string, + // but some people use it outside so we're more lenient + if (c == '\\' || lastc == '\\') + continue; + if (c == '"') { + if (inquote) { + // peek ahead, next char must be "@" + if (validate && i + 1 < len && addr.charAt(i + 1) != '@') + throw new AddressException( + "Quote not at end of local address", addr); + inquote = false; + } else { + if (validate && i != 0) + throw new AddressException( + "Quote not at start of local address", addr); + inquote = true; + } + continue; + } else if (c == '\r') { + // peek ahead, next char must be LF + if (i + 1 < len && addr.charAt(i + 1) != '\n') + throw new AddressException( + "Quoted local address contains CR without LF", addr); + } else if (c == '\n') { + /* + * CRLF followed by whitespace is allowed in a quoted string. + * We allowed naked LF, but ensure LF is always followed by + * whitespace to prevent spoofing the end of the header. + */ + if (i + 1 < len && addr.charAt(i + 1) != ' ' && + addr.charAt(i + 1) != '\t') + throw new AddressException( + "Quoted local address contains newline without whitespace", + addr); + } + if (inquote) + continue; + // dot rules should not be applied to quoted-string + if (c == '.') { + if (i == start) + throw new AddressException( + "Local address starts with dot", addr); + if (lastc == '.') + throw new AddressException( + "Local address contains dot-dot", addr); + } + if (c == '@') { + if (i == 0) + throw new AddressException("Missing local name", addr); + if (lastc == '.') + throw new AddressException( + "Local address ends with dot", addr); + break; // done with local part + } + if (c <= 040 || c == 0177) + throw new AddressException( + "Local address contains control or whitespace", addr); + if (specialsNoDot.indexOf(c) >= 0) + throw new AddressException( + "Local address contains illegal character", addr); + } + if (inquote) + throw new AddressException("Unterminated quote", addr); + + /* + * Done with local part, now check domain. + * + * Note that the MimeMessage class doesn't remember addresses + * as separate objects; it writes them out as headers and then + * parses the headers when the addresses are requested. + * In order to support the case where a "simple" address is used, + * but the address also has a personal name and thus looks like + * it should be a valid RFC822 address when parsed, we only check + * this if we're explicitly called from the validate method. + */ + + if (c != '@') { + if (validate) + throw new AddressException("Missing final '@domain'", addr); + return; + } + + // check for illegal chars in the domain, but ignore domain literals + + start = i + 1; + if (start >= len) + throw new AddressException("Missing domain", addr); + + if (addr.charAt(start) == '.') + throw new AddressException("Domain starts with dot", addr); + boolean inliteral = false; + for (i = start; i < len; i++) { + c = addr.charAt(i); + if (c == '[') { + if (i != start) + throw new AddressException( + "Domain literal not at start of domain", addr); + inliteral = true; // domain literal, don't validate + } else if (c == ']') { + if (i != len - 1) + throw new AddressException( + "Domain literal end not at end of domain", addr); + inliteral = false; + } else if (c <= 040 || c == 0177) { + throw new AddressException( + "Domain contains control or whitespace", addr); + } else { + // RFC 2822 rule + //if (specialsNoDot.indexOf(c) >= 0) + /* + * RFC 1034 rule is more strict + * the full rule is: + * + * ::= | " " + * ::=

+ * This should be a method on String. + */ + private static int indexOfAny(String s, String any) { + return indexOfAny(s, any, 0); + } + + private static int indexOfAny(String s, String any, int start) { + try { + int len = s.length(); + for (int i = start; i < len; i++) { + if (any.indexOf(s.charAt(i)) >= 0) + return i; + } + return -1; + } catch (StringIndexOutOfBoundsException e) { + return -1; + } + } + + /** + * Return the type of this address. The type of an InternetAddress + * is "rfc822". + */ + @Override + public String getType() { + return "rfc822"; + } + + /** + * Set the personal name. If the name contains non US-ASCII + * characters, then the name will be encoded using the specified + * charset as per RFC 2047. If the name contains only US-ASCII + * characters, no encoding is done and the name is used as is. + * + * @param name personal name + * @param charset MIME charset to be used to encode the name as + * per RFC 2047 + * @throws UnsupportedEncodingException if the charset encoding + * fails. + * @see #setPersonal(String) + */ + private void setPersonal(String name, String charset) + throws UnsupportedEncodingException { + personal = name; + if (name != null) + encodedPersonal = MimeUtility.encodeWord(name, charset, null); + else + encodedPersonal = null; + } + + /** + * Get the email address. + * + * @return email address + */ + public String getAddress() { + return address; + } + + /** + * Set the email address. + * + * @param address email address + */ + public void setAddress(String address) { + this.address = address; + } + + /** + * Get the personal name. If the name is encoded as per RFC 2047, + * it is decoded and converted into Unicode. If the decoding or + * conversion fails, the raw data is returned as is. + * + * @return personal name + */ + public String getPersonal() { + if (personal != null) + return personal; + + if (encodedPersonal != null) { + try { + personal = MimeUtility.decodeText(encodedPersonal); + return personal; + } catch (Exception ex) { + // 1. ParseException: either its an unencoded string or + // it can't be parsed + // 2. UnsupportedEncodingException: can't decode it. + return encodedPersonal; + } + } + // No personal or encodedPersonal, return null + return null; + } + + /** + * Set the personal name. If the name contains non US-ASCII + * characters, then the name will be encoded using the platform's + * default charset. If the name contains only US-ASCII characters, + * no encoding is done and the name is used as is. + * + * @param name personal name + * @throws UnsupportedEncodingException if the charset encoding + * fails. + * @see #setPersonal(String name, String charset) + */ + public void setPersonal(String name) + throws UnsupportedEncodingException { + personal = name; + if (name != null) + encodedPersonal = MimeUtility.encodeWord(name); + else + encodedPersonal = null; + } + + /** + * Convert this address into a RFC 822 / RFC 2047 encoded address. + * The resulting string contains only US-ASCII characters, and + * hence is mail-safe. + * + * @return possibly encoded address string + */ + @Override + public String toString() { + String a = address == null ? "" : address; + if (encodedPersonal == null && personal != null) + try { + encodedPersonal = MimeUtility.encodeWord(personal); + } catch (UnsupportedEncodingException ex) { + } + + if (encodedPersonal != null) + return quotePhrase(encodedPersonal) + " <" + a + ">"; + else if (isGroup() || isSimple()) + return a; + else + return "<" + a + ">"; + } + + /** + * Returns a properly formatted address (RFC 822 syntax) of + * Unicode characters. + * + * @return Unicode address string + * @since JavaMail 1.2 + */ + public String toUnicodeString() { + String p = getPersonal(); + if (p != null) + return quotePhrase(p) + " <" + address + ">"; + else if (isGroup() || isSimple()) + return address; + else + return "<" + address + ">"; + } + + /** + * The equality operator. + */ + @Override + public boolean equals(Object a) { + if (!(a instanceof InternetAddress)) + return false; + + String s = ((InternetAddress) a).getAddress(); + if (s == address) + return true; + if (address != null && address.equalsIgnoreCase(s)) + return true; + + return false; + } + + /** + * Compute a hash code for the address. + */ + @Override + public int hashCode() { + if (address == null) + return 0; + else + return address.toLowerCase(Locale.ENGLISH).hashCode(); + } + + /** + * Validate that this address conforms to the syntax rules of + * RFC 822. The current implementation checks many, but not + * all, syntax rules. Note that even though the syntax of + * the address may be correct, there's no guarantee that a + * mailbox of that name exists. + * + * @throws AddressException if the address isn't valid. + * @since JavaMail 1.3 + */ + public void validate() throws AddressException { + if (isGroup()) + getGroup(true); // throw away the result + else + checkAddress(getAddress(), true, true); + } + + /** + * Is this a "simple" address? Simple addresses don't contain quotes + * or any RFC822 special characters other than '@' and '.'. + */ + private boolean isSimple() { + return address == null || indexOfAny(address, specialsNoDotNoAt) < 0; + } + + /** + * Indicates whether this address is an RFC 822 group address. + * Note that a group address is different than the mailing + * list addresses supported by most mail servers. Group addresses + * are rarely used; see RFC 822 for details. + * + * @return true if this address represents a group + * @since JavaMail 1.3 + */ + public boolean isGroup() { + // quick and dirty check + return address != null && address.endsWith(";") && address.indexOf(':') > 0; + } + + /** + * Return the members of a group address. A group may have zero, + * one, or more members. If this address is not a group, null + * is returned. The strict parameter controls whether + * the group list is parsed using strict RFC 822 rules or not. + * The parsing is done using the parseHeader method. + * + * @param strict use strict RFC 822 rules? + * @return array of InternetAddress objects, or null + * @throws AddressException if the group list can't be parsed + * @since JavaMail 1.3 + */ + public InternetAddress[] getGroup(boolean strict) throws AddressException { + String addr = getAddress(); + if (addr == null) + return null; + // groups are of the form "name:addr,addr,...;" + if (!addr.endsWith(";")) + return null; + int ix = addr.indexOf(':'); + if (ix < 0) + return null; + // extract the list + String list = addr.substring(ix + 1, addr.length() - 1); + // parse it and return the individual addresses + return InternetAddress.parseHeader(list, strict); + } + + /* + public static void main(String argv[]) throws Exception { + for (int i = 0; i < argv.length; i++) { + InternetAddress[] a = InternetAddress.parse(argv[i]); + for (int j = 0; j < a.length; j++) { + System.out.println("arg " + i + " address " + j + ": " + a[j]); + System.out.println("\tAddress: " + a[j].getAddress() + + "\tPersonal: " + a[j].getPersonal()); + } + if (a.length > 1) { + System.out.println("address 0 hash code: " + a[0].hashCode()); + System.out.println("address 1 hash code: " + a[1].hashCode()); + if (a[0].hashCode() == a[1].hashCode()) + System.out.println("success, hashcodes equal"); + else + System.out.println("fail, hashcodes not equal"); + if (a[0].equals(a[1])) + System.out.println("success, addresses equal"); + else + System.out.println("fail, addresses not equal"); + if (a[1].equals(a[0])) + System.out.println("success, addresses equal"); + else + System.out.println("fail, addresses not equal"); + } + } + } + */ +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/InternetHeaders.java b/net-mail/src/main/java/jakarta/mail/internet/InternetHeaders.java new file mode 100644 index 0000000..5d658ed --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/InternetHeaders.java @@ -0,0 +1,690 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import jakarta.mail.Header; +import jakarta.mail.MessagingException; +import jakarta.mail.util.LineInputStream; +import jakarta.mail.util.StreamProvider; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; + + +/** + * InternetHeaders is a utility class that manages RFC822 style + * headers. Given an RFC822 format message stream, it reads lines + * until the blank line that indicates end of header. The input stream + * is positioned at the start of the body. The lines are stored + * within the object and can be extracted as either Strings or + * {@link Header} objects.

+ *

+ * This class is mostly intended for service providers. MimeMessage + * and MimeBody use this class for holding their headers. + * + *


A note on RFC822 and MIME headers

+ *

+ * RFC822 and MIME header fields must contain only + * US-ASCII characters. If a header contains non US-ASCII characters, + * it must be encoded as per the rules in RFC 2047. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the setHeader, addHeader, and + * addHeaderLine methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate.

+ *

+ * The current implementation supports the System property + * mail.mime.ignorewhitespacelines, which if set to true + * will cause a line containing only whitespace to be considered + * a blank line terminating the header. + * + * @author John Mani + * @author Bill Shannon + * @see MimeUtility + */ + +public class InternetHeaders { + private static final boolean ignoreWhitespaceLines = + MimeUtility.getBooleanSystemProperty("mail.mime.ignorewhitespacelines", + false); + /** + * The actual list of Headers, including placeholder entries. + * Placeholder entries are Headers with a null value and + * are never seen by clients of the InternetHeaders class. + * Placeholder entries are used to keep track of the preferred + * order of headers. Headers are never actually removed from + * the list, they're converted into placeholder entries. + * New headers are added after existing headers of the same name + * (or before in the case of Received and + * Return-Path headers). If no existing header + * or placeholder for the header is found, new headers are + * added after the special placeholder with the name ":". + * + * @since JavaMail 1.4 + */ + protected List headers; + + /** + * Create an empty InternetHeaders object. Placeholder entries + * are inserted to indicate the preferred order of headers. + */ + public InternetHeaders() { + headers = new ArrayList<>(40); + headers.add(new InternetHeader("Return-Path", null)); + headers.add(new InternetHeader("Received", null)); + headers.add(new InternetHeader("Resent-Date", null)); + headers.add(new InternetHeader("Resent-From", null)); + headers.add(new InternetHeader("Resent-Sender", null)); + headers.add(new InternetHeader("Resent-To", null)); + headers.add(new InternetHeader("Resent-Cc", null)); + headers.add(new InternetHeader("Resent-Bcc", null)); + headers.add(new InternetHeader("Resent-Message-Id", null)); + headers.add(new InternetHeader("Date", null)); + headers.add(new InternetHeader("From", null)); + headers.add(new InternetHeader("Sender", null)); + headers.add(new InternetHeader("Reply-To", null)); + headers.add(new InternetHeader("To", null)); + headers.add(new InternetHeader("Cc", null)); + headers.add(new InternetHeader("Bcc", null)); + headers.add(new InternetHeader("Message-Id", null)); + headers.add(new InternetHeader("In-Reply-To", null)); + headers.add(new InternetHeader("References", null)); + headers.add(new InternetHeader("Subject", null)); + headers.add(new InternetHeader("Comments", null)); + headers.add(new InternetHeader("Keywords", null)); + headers.add(new InternetHeader("Errors-To", null)); + headers.add(new InternetHeader("MIME-Version", null)); + headers.add(new InternetHeader("Content-Type", null)); + headers.add(new InternetHeader("Content-Transfer-Encoding", null)); + headers.add(new InternetHeader("Content-MD5", null)); + headers.add(new InternetHeader(":", null)); + headers.add(new InternetHeader("Content-Length", null)); + headers.add(new InternetHeader("Status", null)); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. The input + * stream is left positioned at the start of the body. The + * header lines are stored internally.

+ *

+ * For efficiency, wrap a BufferedInputStream around the actual + * input stream and pass it as the parameter.

+ *

+ * No placeholder entries are inserted; the original order of + * the headers is preserved. + * + * @param is RFC822 input stream + * @throws MessagingException for any I/O error reading the stream + */ + public InternetHeaders(InputStream is) throws MessagingException { + this(is, false); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. The input + * stream is left positioned at the start of the body. The + * header lines are stored internally.

+ *

+ * For efficiency, wrap a BufferedInputStream around the actual + * input stream and pass it as the parameter.

+ *

+ * No placeholder entries are inserted; the original order of + * the headers is preserved. + * + * @param is RFC822 input stream + * @param allowutf8 if UTF-8 encoded headers are allowed + * @throws MessagingException for any I/O error reading the stream + * @since JavaMail 1.6 + */ + @SuppressWarnings("this-escape") + public InternetHeaders(InputStream is, boolean allowutf8) + throws MessagingException { + headers = new ArrayList<>(40); + load(is, allowutf8); + } + + /** + * Is this line an empty (blank) line? + */ + private static boolean isEmpty(String line) { + return line.isEmpty() || (ignoreWhitespaceLines && line.trim().isEmpty()); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. Store the + * header lines inside this InternetHeaders object. The order + * of header lines is preserved.

+ *

+ * Note that the header lines are added into this InternetHeaders + * object, so any existing headers in this object will not be + * affected. Headers are added to the end of the existing list + * of headers, in order. + * + * @param is RFC822 input stream + * @throws MessagingException for any I/O error reading the stream + */ + public void load(InputStream is) throws MessagingException { + load(is, false); + } + + /** + * Read and parse the given RFC822 message stream till the + * blank line separating the header from the body. Store the + * header lines inside this InternetHeaders object. The order + * of header lines is preserved.

+ *

+ * Note that the header lines are added into this InternetHeaders + * object, so any existing headers in this object will not be + * affected. Headers are added to the end of the existing list + * of headers, in order. + * + * @param is RFC822 input stream + * @param allowutf8 if UTF-8 encoded headers are allowed + * @throws MessagingException for any I/O error reading the stream + * @since JavaMail 1.6 + */ + public void load(InputStream is, boolean allowutf8) + throws MessagingException { + // Read header lines until a blank line. It is valid + // to have BodyParts with no header lines. + String line; + LineInputStream lis = StreamProvider.provider().inputLineStream(is, allowutf8); + String prevline = null; // the previous header line, as a string + // a buffer to accumulate the header in, when we know it's needed + StringBuilder lineBuffer = new StringBuilder(); + + try { + // if the first line being read is a continuation line, + // we ignore it if it's otherwise empty or we treat it as + // a non-continuation line if it has non-whitespace content + boolean first = true; + do { + line = lis.readLine(); + if (line != null && + (line.startsWith(" ") || line.startsWith("\t"))) { + // continuation of header + if (prevline != null) { + lineBuffer.append(prevline); + prevline = null; + } + if (first) { + String lt = line.trim(); + if (lt.length() > 0) + lineBuffer.append(lt); + } else { + if (lineBuffer.length() > 0) + lineBuffer.append("\r\n"); + lineBuffer.append(line); + } + } else { + // new header + if (prevline != null) + addHeaderLine(prevline); + else if (lineBuffer.length() > 0) { + // store previous header first + addHeaderLine(lineBuffer.toString()); + lineBuffer.setLength(0); + } + prevline = line; + } + first = false; + } while (line != null && !isEmpty(line)); + } catch (IOException ioex) { + throw new MessagingException("Error in input stream", ioex); + } + } + + /** + * Return all the values for the specified header. The + * values are String objects. Returns null + * if no headers with the specified name exist. + * + * @param name header name + * @return array of header values, or null if none + */ + public String[] getHeader(String name) { + Iterator e = headers.iterator(); + // XXX - should we just step through in index order? + List v = new ArrayList<>(); // accumulate return values + + while (e.hasNext()) { + InternetHeader h = e.next(); + if (name.equalsIgnoreCase(h.getName()) && h.line != null) { + v.add(h.getValue()); + } + } + if (v.size() == 0) + return (null); + // convert List to an array for return + String[] r = new String[v.size()]; + r = v.toArray(r); + return (r); + } + + /** + * 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. Returns null + * if no headers with the specified name exist. + * + * @param delimiter delimiter + * @param name header name + * @return the value fields for all headers with + * this name, or null if none + */ + public String getHeader(String name, String delimiter) { + String[] s = getHeader(name); + + if (s == null) + return null; + + if ((s.length == 1) || delimiter == null) + return s[0]; + + StringBuilder r = new StringBuilder(s[0]); + for (int i = 1; i < s.length; i++) { + r.append(delimiter); + r.append(s[i]); + } + return r.toString(); + } + + /** + * Change the first header line that matches name + * to have value, adding a new header if no existing header + * matches. Remove all matching headers but the first.

+ *

+ * Note that RFC822 headers can only contain US-ASCII characters + * + * @param name header name + * @param value header value + */ + public void setHeader(String name, String value) { + boolean found = false; + + for (int i = 0; i < headers.size(); i++) { + InternetHeader h = headers.get(i); + if (name.equalsIgnoreCase(h.getName())) { + if (!found) { + int j; + if (h.line != null && (j = h.line.indexOf(':')) >= 0) { + h.line = h.line.substring(0, j + 1) + " " + value; + // preserves capitalization, spacing + } else { + h.line = name + ": " + value; + } + found = true; + } else { + headers.remove(i); + i--; // have to look at i again + } + } + } + + if (!found) { + addHeader(name, value); + } + } + + /** + * Add a header with the specified name and value to the header list.

+ *

+ * The current implementation knows about the preferred order of most + * well-known headers and will insert headers in that order. In + * addition, it knows that Received headers should be + * inserted in reverse order (newest before oldest), and that they + * should appear at the beginning of the headers, preceeded only by + * a possible Return-Path header.

+ *

+ * Note that RFC822 headers can only contain US-ASCII characters. + * + * @param name header name + * @param value header value + */ + public void addHeader(String name, String value) { + int pos = headers.size(); + boolean addReverse = + name.equalsIgnoreCase("Received") || + name.equalsIgnoreCase("Return-Path"); + if (addReverse) + pos = 0; + for (int i = headers.size() - 1; i >= 0; i--) { + InternetHeader h = headers.get(i); + if (name.equalsIgnoreCase(h.getName())) { + if (addReverse) { + pos = i; + } else { + headers.add(i + 1, new InternetHeader(name, value)); + return; + } + } + // marker for default place to add new headers + if (!addReverse && h.getName().equals(":")) + pos = i; + } + headers.add(pos, new InternetHeader(name, value)); + } + + /** + * Remove all header entries that match the given name + * + * @param name header name + */ + public void removeHeader(String name) { + for (int i = 0; i < headers.size(); i++) { + InternetHeader h = headers.get(i); + if (name.equalsIgnoreCase(h.getName())) { + h.line = null; + //headers.remove(i); + //i--; // have to look at i again + } + } + } + + /** + * Return all the headers as an Enumeration of + * {@link Header} objects. + * + * @return Enumeration of Header objects + */ + public Enumeration

getAllHeaders() { + return (new MatchHeaderEnum(headers, null, false)); + } + + /** + * Return all matching {@link Header} objects. + * + * @param names the headers to return + * @return Enumeration of matching Header objects + */ + public Enumeration
getMatchingHeaders(String[] names) { + return (new MatchHeaderEnum(headers, names, true)); + } + + /** + * Return all non-matching {@link Header} objects. + * + * @param names the headers to not return + * @return Enumeration of non-matching Header objects + */ + public Enumeration
getNonMatchingHeaders(String[] names) { + return (new MatchHeaderEnum(headers, names, false)); + } + + /** + * Add an RFC822 header line to the header store. + * If the line starts with a space or tab (a continuation line), + * add it to the last header line in the list. Otherwise, + * append the new header line to the list.

+ *

+ * Note that RFC822 headers can only contain US-ASCII characters + * + * @param line raw RFC822 header line + */ + public void addHeaderLine(String line) { + try { + char c = line.charAt(0); + if (c == ' ' || c == '\t') { + InternetHeader h = headers.get(headers.size() - 1); + h.line += "\r\n" + line; + } else + headers.add(new InternetHeader(line)); + } catch (StringIndexOutOfBoundsException e) { + // line is empty, ignore it + return; + } catch (NoSuchElementException e) { + // XXX - list is empty? + } + } + + /** + * Return all the header lines as an Enumeration of Strings. + * + * @return Enumeration of Strings of all header lines + */ + public Enumeration getAllHeaderLines() { + return (getNonMatchingHeaderLines(null)); + } + + /** + * Return all matching header lines as an Enumeration of Strings. + * + * @param names the headers to return + * @return Enumeration of Strings of all matching header lines + */ + public Enumeration getMatchingHeaderLines(String[] names) { + return (new MatchStringEnum(headers, names, true)); + } + + /** + * Return all non-matching header lines + * + * @param names the headers to not return + * @return Enumeration of Strings of all non-matching header lines + */ + public Enumeration getNonMatchingHeaderLines(String[] names) { + return (new MatchStringEnum(headers, names, false)); + } + + /** + * An individual internet header. This class is only used by + * subclasses of InternetHeaders.

+ *

+ * An InternetHeader object with a null value is used as a placeholder + * for headers of that name, to preserve the order of headers. + * A placeholder InternetHeader object with a name of ":" marks + * the location in the list of headers where new headers are + * added by default. + * + * @since JavaMail 1.4 + */ + protected static final class InternetHeader extends Header { + /* + * Note that the value field from the superclass + * isn't used in this class. We extract the value + * from the line field as needed. We store the line + * rather than just the value to ensure that we can + * get back the exact original line, with the original + * whitespace, etc. + */ + String line; // the entire RFC822 header "line", + // or null if placeholder + + /** + * Constructor that takes a line and splits out + * the header name. + * + * @param l the header line + */ + public InternetHeader(String l) { + super("", ""); // XXX - we'll change it later + int i = l.indexOf(':'); + if (i < 0) { + // should never happen + name = l.trim(); + } else { + name = l.substring(0, i).trim(); + } + line = l; + } + + /** + * Constructor that takes a header name and value. + * + * @param n the name of the header + * @param v the value of the header + */ + public InternetHeader(String n, String v) { + super(n, ""); + if (v != null) + line = n + ": " + v; + else + line = null; + } + + /** + * Return the "value" part of the header line. + */ + @Override + public String getValue() { + int i = line.indexOf(':'); + if (i < 0) + return line; + // skip whitespace after ':' + int j; + for (j = i + 1; j < line.length(); j++) { + char c = line.charAt(j); + if (!(c == ' ' || c == '\t' || c == '\r' || c == '\n')) + break; + } + return line.substring(j); + } + } + + /* + * The enumeration object used to enumerate an + * InternetHeaders object. Can return + * either a String or a Header object. + */ + static class MatchEnum { + private Iterator e; // enum object of headers List + // XXX - is this overkill? should we step through in index + // order instead? + private String[] names; // names to match, or not + private boolean match; // return matching headers? + private boolean want_line; // return header lines? + private InternetHeader next_header; // the next header to be returned + + /* + * Constructor. Initialize the enumeration for the entire + * List of headers, the set of headers, whether to return + * matching or non-matching headers, and whether to return + * header lines or Header objects. + */ + MatchEnum(List v, String[] n, boolean m, boolean l) { + e = v.iterator(); + names = n; + match = m; + want_line = l; + next_header = null; + } + + /* + * Any more elements in this enumeration? + */ + public boolean hasMoreElements() { + // if necessary, prefetch the next matching header, + // and remember it. + if (next_header == null) + next_header = nextMatch(); + return next_header != null; + } + + /* + * Return the next element. + */ + public Object nextElement() { + if (next_header == null) + next_header = nextMatch(); + + if (next_header == null) + throw new NoSuchElementException("No more headers"); + + InternetHeader h = next_header; + next_header = null; + if (want_line) + return h.line; + else + return new Header(h.getName(), h.getValue()); + } + + /* + * Return the next Header object according to the match + * criteria, or null if none left. + */ + private InternetHeader nextMatch() { + next: + while (e.hasNext()) { + InternetHeader h = e.next(); + + // skip "place holder" headers + if (h.line == null) + continue; + + // if no names to match against, return appropriately + if (names == null) + return match ? null : h; + + // check whether this header matches any of the names + for (int i = 0; i < names.length; i++) { + if (names[i].equalsIgnoreCase(h.getName())) { + if (match) + return h; + else + // found a match, but we're + // looking for non-matches. + // try next header. + continue next; + } + } + // found no matches. if that's what we wanted, return it. + if (!match) + return h; + } + return null; + } + } + + static class MatchStringEnum extends MatchEnum + implements Enumeration { + + MatchStringEnum(List v, String[] n, boolean m) { + super(v, n, m, true); + } + + @Override + public String nextElement() { + return (String) super.nextElement(); + } + + } + + static class MatchHeaderEnum extends MatchEnum + implements Enumeration

{ + + MatchHeaderEnum(List v, String[] n, boolean m) { + super(v, n, m, false); + } + + @Override + public Header nextElement() { + return (Header) super.nextElement(); + } + + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/MailDateFormat.java b/net-mail/src/main/java/jakarta/mail/internet/MailDateFormat.java new file mode 100644 index 0000000..f48891b --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/MailDateFormat.java @@ -0,0 +1,990 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import java.text.DateFormatSymbols; +import java.text.FieldPosition; +import java.text.NumberFormat; +import java.text.ParseException; +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Calendar; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Formats and parses date specification based on + * RFC 2822.

+ *

+ * This class does not support methods that influence the format. It always + * formats the date based on the specification below.

+ *

+ * 3.3. Date and Time Specification + *

+ * Date and time occur in several header fields. This section specifies + * the syntax for a full date and time specification. Though folding + * white space is permitted throughout the date-time specification, it is + * RECOMMENDED that a single space be used in each place that FWS appears + * (whether it is required or optional); some older implementations may + * not interpret other occurrences of folding white space correctly. + *

+ * date-time       =       [ day-of-week "," ] date FWS time [CFWS]
+ *
+ * day-of-week     =       ([FWS] day-name) / obs-day-of-week
+ *
+ * day-name        =       "Mon" / "Tue" / "Wed" / "Thu" /
+ *                         "Fri" / "Sat" / "Sun"
+ *
+ * date            =       day month year
+ *
+ * year            =       4*DIGIT / obs-year
+ *
+ * month           =       (FWS month-name FWS) / obs-month
+ *
+ * month-name      =       "Jan" / "Feb" / "Mar" / "Apr" /
+ *                         "May" / "Jun" / "Jul" / "Aug" /
+ *                         "Sep" / "Oct" / "Nov" / "Dec"
+ *
+ * day             =       ([FWS] 1*2DIGIT) / obs-day
+ *
+ * time            =       time-of-day FWS zone
+ *
+ * time-of-day     =       hour ":" minute [ ":" second ]
+ *
+ * hour            =       2DIGIT / obs-hour
+ *
+ * minute          =       2DIGIT / obs-minute
+ *
+ * second          =       2DIGIT / obs-second
+ *
+ * zone            =       (( "+" / "-" ) 4DIGIT) / obs-zone
+ * 
+ * The day is the numeric day of the month. The year is any numeric year + * 1900 or later. + *

+ * The time-of-day specifies the number of hours, minutes, and optionally + * seconds since midnight of the date indicated. + *

+ * The date and time-of-day SHOULD express local time. + *

+ * The zone specifies the offset from Coordinated Universal Time (UTC, + * formerly referred to as "Greenwich Mean Time") that the date and + * time-of-day represent. The "+" or "-" indicates whether the + * time-of-day is ahead of (i.e., east of) or behind (i.e., west of) + * Universal Time. The first two digits indicate the number of hours + * difference from Universal Time, and the last two digits indicate the + * number of minutes difference from Universal Time. (Hence, +hhmm means + * +(hh * 60 + mm) minutes, and -hhmm means -(hh * 60 + mm) minutes). The + * form "+0000" SHOULD be used to indicate a time zone at Universal Time. + * Though "-0000" also indicates Universal Time, it is used to indicate + * that the time was generated on a system that may be in a local time + * zone other than Universal Time and therefore indicates that the + * date-time contains no information about the local time zone. + *

+ * A date-time specification MUST be semantically valid. That is, the + * day-of-the-week (if included) MUST be the day implied by the date, the + * numeric day-of-month MUST be between 1 and the number of days allowed + * for the specified month (in the specified year), the time-of-day MUST + * be in the range 00:00:00 through 23:59:60 (the number of seconds + * allowing for a leap second; see [STD12]), and the zone MUST be within + * the range -9959 through +9959. + * + *

Synchronization

+ * + *

+ * Date formats are not synchronized. + * It is recommended to create separate format instances for each thread. + * If multiple threads access a format concurrently, it must be synchronized + * externally. + * + * @author Anthony Vanelverdinghe + * @author Max Spivak + * @since JavaMail 1.2 + */ +@SuppressWarnings("serial") +public class MailDateFormat extends SimpleDateFormat { + + private static final String PATTERN = "EEE, d MMM yyyy HH:mm:ss Z (z)"; + + private static final Logger logger = Logger.getLogger(MailDateFormat.class.getName()); + + private static final int UNKNOWN_DAY_NAME = -1; + private static final TimeZone UTC = TimeZone.getTimeZone("UTC"); + private static final int LEAP_SECOND = 60; + + /** + * Create a new date format for the RFC2822 specification with lenient + * parsing. + */ + public MailDateFormat() { + super(PATTERN, Locale.US); + } + + /** + * Formats the given date in the format specified by + * RFC 2822 in the current TimeZone. + * + * @param date the Date object + * @param dateStrBuf the formatted string + * @param fieldPosition the current field position + * @return StringBuffer the formatted String + * @since JavaMail 1.2 + */ + @Override + public StringBuffer format(Date date, StringBuffer dateStrBuf, + FieldPosition fieldPosition) { + return super.format(date, dateStrBuf, fieldPosition); + } + + /** + * Parses the given date in the format specified by + * RFC 2822. + *

    + *
  • With strict parsing, obs-* tokens are unsupported. Lenient parsing + * supports obs-year and obs-zone, with the exception of the 1-character + * military time zones. + *
  • The optional CFWS token at the end is not parsed. + *
  • RFC 2822 specifies that a zone of "-0000" indicates that the + * date-time contains no information about the local time zone. This class + * uses the UTC time zone in this case. + *
+ * + * @param text the formatted date to be parsed + * @param pos the current parse position + * @return Date the parsed date. In case of error, returns null. + * @since JavaMail 1.2 + */ + @Override + public Date parse(String text, ParsePosition pos) { + if (text == null || pos == null) { + throw new NullPointerException(); + } else if (0 > pos.getIndex() || pos.getIndex() >= text.length()) { + return null; + } + + return isLenient() + ? new Rfc2822LenientParser(text, pos).parse() + : new Rfc2822StrictParser(text, pos).parse(); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific calendar. + * + * @throws UnsupportedOperationException if this method is invoked + */ + @Override + public void setCalendar(Calendar newCalendar) { + throw new UnsupportedOperationException("Method " + + "setCalendar() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific number format. + * + * @throws UnsupportedOperationException if this method is invoked + */ + @Override + public void setNumberFormat(NumberFormat newNumberFormat) { + throw new UnsupportedOperationException("Method " + + "setNumberFormat() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific pattern. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void applyLocalizedPattern(String pattern) { + throw new UnsupportedOperationException("Method " + + "applyLocalizedPattern() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates a specific pattern. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void applyPattern(String pattern) { + throw new UnsupportedOperationException("Method " + + "applyPattern() shouldn't be called"); + } + + /** + * This method allows serialization to change the pattern. + */ + private void superApplyPattern(String pattern) { + super.applyPattern(pattern); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates another strategy for interpreting + * 2-digits years. + * + * @return the start of the 100-year period into which two digit years are + * parsed + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public Date get2DigitYearStart() { + throw new UnsupportedOperationException("Method " + + "get2DigitYearStart() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates another strategy for interpreting + * 2-digits years. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void set2DigitYearStart(Date startDate) { + throw new UnsupportedOperationException("Method " + + "set2DigitYearStart() shouldn't be called"); + } + + /** + * This method always throws an UnsupportedOperationException and should not + * be used because RFC 2822 mandates specific date format symbols. + * + * @throws UnsupportedOperationException if this method is invoked + * @since JavaMail 1.6 + */ + @Override + public void setDateFormatSymbols(DateFormatSymbols newFormatSymbols) { + throw new UnsupportedOperationException("Method " + + "setDateFormatSymbols() shouldn't be called"); + } + + /** + * Returns the date, as specified by the parameters. + * + * @return the date, as specified by the parameters + * @throws IllegalArgumentException if this instance's Calendar is + * non-lenient and any of the parameters have invalid values, or if dayName + * is not consistent with day-month-year + */ + private Date toDate(int dayName, int day, int month, int year, + int hour, int minute, int second, int zone) { + if (second == LEAP_SECOND) { + second = 59; + } + + TimeZone tz = calendar.getTimeZone(); + try { + calendar.setTimeZone(UTC); + calendar.clear(); + calendar.set(year, month, day, hour, minute, second); + + if (dayName == UNKNOWN_DAY_NAME + || dayName == calendar.get(Calendar.DAY_OF_WEEK)) { + calendar.add(Calendar.MINUTE, zone); + return calendar.getTime(); + } else { + throw new IllegalArgumentException("Inconsistent day-name"); + } + } finally { + calendar.setTimeZone(tz); + } + } + + /** + * This class provides the building blocks for date parsing. + *

+ * It has the following invariants: + *

    + *
  • no exceptions are thrown, except for java.text.ParseException from + * parse* methods + *
  • when parse* throws ParseException OR get* returns INVALID_CHAR OR + * skip* returns false OR peek* is invoked, then pos.getIndex() on method + * exit is the same as it was on method entry + *
+ */ + private static abstract class AbstractDateParser { + + static final int INVALID_CHAR = -1; + static final int MAX_YEAR_DIGITS = 8; // guarantees that: + // year < new GregorianCalendar().getMaximum(Calendar.YEAR) + + final String text; + final ParsePosition pos; + + AbstractDateParser(String text, ParsePosition pos) { + this.text = text; + this.pos = pos; + } + + final Date parse() { + int startPosition = pos.getIndex(); + try { + return tryParse(); + } catch (Exception e) { // == ParseException | RuntimeException e + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "Bad date: '" + text + "'", e); + } + pos.setErrorIndex(pos.getIndex()); + pos.setIndex(startPosition); + return null; + } + } + + abstract Date tryParse() throws ParseException; + + /** + * @return the java.util.Calendar constant for the parsed day name + */ + final int parseDayName() throws ParseException { + switch (getChar()) { + case 'S': + if (skipPair('u', 'n')) { + return Calendar.SUNDAY; + } else if (skipPair('a', 't')) { + return Calendar.SATURDAY; + } + break; + case 'T': + if (skipPair('u', 'e')) { + return Calendar.TUESDAY; + } else if (skipPair('h', 'u')) { + return Calendar.THURSDAY; + } + break; + case 'M': + if (skipPair('o', 'n')) { + return Calendar.MONDAY; + } + break; + case 'W': + if (skipPair('e', 'd')) { + return Calendar.WEDNESDAY; + } + break; + case 'F': + if (skipPair('r', 'i')) { + return Calendar.FRIDAY; + } + break; + case INVALID_CHAR: + throw new ParseException("Invalid day-name", + pos.getIndex()); + } + pos.setIndex(pos.getIndex() - 1); + throw new ParseException("Invalid day-name", pos.getIndex()); + } + + /** + * @return the java.util.Calendar constant for the parsed month name + */ + @SuppressWarnings("fallthrough") + final int parseMonthName(boolean caseSensitive) throws ParseException { + switch (getChar()) { + case 'j': + if (caseSensitive) { + break; + } + case 'J': + if (skipChar('u') || (!caseSensitive && skipChar('U'))) { + if (skipChar('l') || (!caseSensitive + && skipChar('L'))) { + return Calendar.JULY; + } else if (skipChar('n') || (!caseSensitive + && skipChar('N'))) { + return Calendar.JUNE; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } else if (skipPair('a', 'n') || (!caseSensitive + && skipAlternativePair('a', 'A', 'n', 'N'))) { + return Calendar.JANUARY; + } + break; + case 'm': + if (caseSensitive) { + break; + } + case 'M': + if (skipChar('a') || (!caseSensitive && skipChar('A'))) { + if (skipChar('r') || (!caseSensitive + && skipChar('R'))) { + return Calendar.MARCH; + } else if (skipChar('y') || (!caseSensitive + && skipChar('Y'))) { + return Calendar.MAY; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } + break; + case 'a': + if (caseSensitive) { + break; + } + case 'A': + if (skipPair('u', 'g') || (!caseSensitive + && skipAlternativePair('u', 'U', 'g', 'G'))) { + return Calendar.AUGUST; + } else if (skipPair('p', 'r') || (!caseSensitive + && skipAlternativePair('p', 'P', 'r', 'R'))) { + return Calendar.APRIL; + } + break; + case 'd': + if (caseSensitive) { + break; + } + case 'D': + if (skipPair('e', 'c') || (!caseSensitive + && skipAlternativePair('e', 'E', 'c', 'C'))) { + return Calendar.DECEMBER; + } + break; + case 'o': + if (caseSensitive) { + break; + } + case 'O': + if (skipPair('c', 't') || (!caseSensitive + && skipAlternativePair('c', 'C', 't', 'T'))) { + return Calendar.OCTOBER; + } + break; + case 's': + if (caseSensitive) { + break; + } + case 'S': + if (skipPair('e', 'p') || (!caseSensitive + && skipAlternativePair('e', 'E', 'p', 'P'))) { + return Calendar.SEPTEMBER; + } + break; + case 'n': + if (caseSensitive) { + break; + } + case 'N': + if (skipPair('o', 'v') || (!caseSensitive + && skipAlternativePair('o', 'O', 'v', 'V'))) { + return Calendar.NOVEMBER; + } + break; + case 'f': + if (caseSensitive) { + break; + } + case 'F': + if (skipPair('e', 'b') || (!caseSensitive + && skipAlternativePair('e', 'E', 'b', 'B'))) { + return Calendar.FEBRUARY; + } + break; + case INVALID_CHAR: + throw new ParseException("Invalid month", pos.getIndex()); + } + pos.setIndex(pos.getIndex() - 1); + throw new ParseException("Invalid month", pos.getIndex()); + } + + /** + * @return the number of minutes to be added to the time in the local + * time zone, in order to obtain the equivalent time in the UTC time + * zone. Returns 0 if the date-time contains no information about the + * local time zone. + */ + final int parseZoneOffset() throws ParseException { + int sign = getChar(); + if (sign == '+' || sign == '-') { + int offset = parseAsciiDigits(4, 4, true); + if (!isValidZoneOffset(offset)) { + pos.setIndex(pos.getIndex() - 5); + throw new ParseException("Invalid zone", pos.getIndex()); + } + + return ((sign == '+') ? -1 : 1) + * (offset / 100 * 60 + offset % 100); + } else if (sign != INVALID_CHAR) { + pos.setIndex(pos.getIndex() - 1); + } + throw new ParseException("Invalid zone", pos.getIndex()); + } + + boolean isValidZoneOffset(int offset) { + return (offset % 100) < 60; + } + + final int parseAsciiDigits(int count) throws ParseException { + return parseAsciiDigits(count, count); + } + + final int parseAsciiDigits(int min, int max) throws ParseException { + return parseAsciiDigits(min, max, false); + } + + final int parseAsciiDigits(int min, int max, boolean isEOF) + throws ParseException { + int result = 0; + int nbDigitsParsed = 0; + while (nbDigitsParsed < max && peekAsciiDigit()) { + result = result * 10 + getAsciiDigit(); + nbDigitsParsed++; + } + + if ((nbDigitsParsed < min) + || (nbDigitsParsed == max && !isEOF && peekAsciiDigit())) { + pos.setIndex(pos.getIndex() - nbDigitsParsed); + } else { + return result; + } + + String range = (min == max) + ? Integer.toString(min) + : "between " + min + " and " + max; + throw new ParseException("Invalid input: expected " + + range + " ASCII digits", pos.getIndex()); + } + + final void parseFoldingWhiteSpace() throws ParseException { + if (!skipFoldingWhiteSpace()) { + throw new ParseException("Invalid input: expected FWS", + pos.getIndex()); + } + } + + final void parseChar(char ch) throws ParseException { + if (!skipChar(ch)) { + throw new ParseException("Invalid input: expected '" + ch + "'", + pos.getIndex()); + } + } + + final int getAsciiDigit() { + int ch = getChar(); + if ('0' <= ch && ch <= '9') { + return Character.digit((char) ch, 10); + } else { + if (ch != INVALID_CHAR) { + pos.setIndex(pos.getIndex() - 1); + } + return INVALID_CHAR; + } + } + + final int getChar() { + if (pos.getIndex() < text.length()) { + char ch = text.charAt(pos.getIndex()); + pos.setIndex(pos.getIndex() + 1); + return ch; + } else { + return INVALID_CHAR; + } + } + + boolean skipFoldingWhiteSpace() { + // fast paths: a single ASCII space or no FWS + if (skipChar(' ')) { + if (!peekFoldingWhiteSpace()) { + return true; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } else if (!peekFoldingWhiteSpace()) { + return false; + } + + // normal path + int startIndex = pos.getIndex(); + if (skipWhiteSpace()) { + while (skipNewline()) { + if (!skipWhiteSpace()) { + pos.setIndex(startIndex); + return false; + } + } + return true; + } else if (skipNewline() && skipWhiteSpace()) { + return true; + } else { + pos.setIndex(startIndex); + return false; + } + } + + final boolean skipWhiteSpace() { + int startIndex = pos.getIndex(); + while (skipAlternative(' ', '\t')) { /* empty */ } + return pos.getIndex() > startIndex; + } + + final boolean skipNewline() { + return skipPair('\r', '\n'); + } + + final boolean skipAlternativeTriple( + char firstStandard, char firstAlternative, + char secondStandard, char secondAlternative, + char thirdStandard, char thirdAlternative + ) { + if (skipAlternativePair(firstStandard, firstAlternative, + secondStandard, secondAlternative)) { + if (skipAlternative(thirdStandard, thirdAlternative)) { + return true; + } else { + pos.setIndex(pos.getIndex() - 2); + } + } + return false; + } + + final boolean skipAlternativePair( + char firstStandard, char firstAlternative, + char secondStandard, char secondAlternative + ) { + if (skipAlternative(firstStandard, firstAlternative)) { + if (skipAlternative(secondStandard, secondAlternative)) { + return true; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } + return false; + } + + final boolean skipAlternative(char standard, char alternative) { + return skipChar(standard) || skipChar(alternative); + } + + final boolean skipPair(char first, char second) { + if (skipChar(first)) { + if (skipChar(second)) { + return true; + } else { + pos.setIndex(pos.getIndex() - 1); + } + } + return false; + } + + final boolean skipChar(char ch) { + if (pos.getIndex() < text.length() + && text.charAt(pos.getIndex()) == ch) { + pos.setIndex(pos.getIndex() + 1); + return true; + } else { + return false; + } + } + + final boolean peekAsciiDigit() { + return (pos.getIndex() < text.length() + && '0' <= text.charAt(pos.getIndex()) + && text.charAt(pos.getIndex()) <= '9'); + } + + boolean peekFoldingWhiteSpace() { + return (pos.getIndex() < text.length() + && (text.charAt(pos.getIndex()) == ' ' + || text.charAt(pos.getIndex()) == '\t' + || text.charAt(pos.getIndex()) == '\r')); + } + + final boolean peekChar(char ch) { + return (pos.getIndex() < text.length() + && text.charAt(pos.getIndex()) == ch); + } + + } + + private class Rfc2822StrictParser extends AbstractDateParser { + + Rfc2822StrictParser(String text, ParsePosition pos) { + super(text, pos); + } + + @Override + Date tryParse() throws ParseException { + int dayName = parseOptionalBegin(); + + int day = parseDay(); + int month = parseMonth(); + int year = parseYear(); + + parseFoldingWhiteSpace(); + + int hour = parseHour(); + parseChar(':'); + int minute = parseMinute(); + int second = (skipChar(':')) ? parseSecond() : 0; + + parseFwsBetweenTimeOfDayAndZone(); + + int zone = parseZone(); + + try { + return MailDateFormat.this.toDate(dayName, day, month, year, + hour, minute, second, zone); + } catch (IllegalArgumentException e) { + throw new ParseException("Invalid input: some of the calendar " + + "fields have invalid values, or day-name is " + + "inconsistent with date", pos.getIndex()); + } + } + + /** + * @return the java.util.Calendar constant for the parsed day name, or + * UNKNOWN_DAY_NAME iff the begin is missing + */ + int parseOptionalBegin() throws ParseException { + int dayName; + if (!peekAsciiDigit()) { + skipFoldingWhiteSpace(); + dayName = parseDayName(); + parseChar(','); + } else { + dayName = UNKNOWN_DAY_NAME; + } + return dayName; + } + + int parseDay() throws ParseException { + skipFoldingWhiteSpace(); + return parseAsciiDigits(1, 2); + } + + /** + * @return the java.util.Calendar constant for the parsed month name + */ + int parseMonth() throws ParseException { + parseFwsInMonth(); + int month = parseMonthName(isMonthNameCaseSensitive()); + parseFwsInMonth(); + return month; + } + + void parseFwsInMonth() throws ParseException { + parseFoldingWhiteSpace(); + } + + boolean isMonthNameCaseSensitive() { + return true; + } + + int parseYear() throws ParseException { + int year = parseAsciiDigits(4, MAX_YEAR_DIGITS); + if (year >= 1900) { + return year; + } else { + pos.setIndex(pos.getIndex() - 4); + while (text.charAt(pos.getIndex() - 1) == '0') { + pos.setIndex(pos.getIndex() - 1); + } + throw new ParseException("Invalid year", pos.getIndex()); + } + } + + int parseHour() throws ParseException { + return parseAsciiDigits(2); + } + + int parseMinute() throws ParseException { + return parseAsciiDigits(2); + } + + int parseSecond() throws ParseException { + return parseAsciiDigits(2); + } + + void parseFwsBetweenTimeOfDayAndZone() throws ParseException { + parseFoldingWhiteSpace(); + } + + int parseZone() throws ParseException { + return parseZoneOffset(); + } + + } + + private class Rfc2822LenientParser extends Rfc2822StrictParser { + + private Boolean hasDefaultFws; + + Rfc2822LenientParser(String text, ParsePosition pos) { + super(text, pos); + } + + @Override + int parseOptionalBegin() { + while (pos.getIndex() < text.length() && !peekAsciiDigit()) { + pos.setIndex(pos.getIndex() + 1); + } + + return UNKNOWN_DAY_NAME; + } + + @Override + int parseDay() throws ParseException { + skipFoldingWhiteSpace(); + return parseAsciiDigits(1, 3); + } + + @Override + void parseFwsInMonth() throws ParseException { + // '-' is allowed to accomodate for the date format as specified in + // RFC 3501 + if (hasDefaultFws == null) { + hasDefaultFws = !skipChar('-'); + skipFoldingWhiteSpace(); + } else if (hasDefaultFws) { + skipFoldingWhiteSpace(); + } else { + parseChar('-'); + } + } + + @Override + boolean isMonthNameCaseSensitive() { + return false; + } + + @Override + int parseYear() throws ParseException { + int year = parseAsciiDigits(1, MAX_YEAR_DIGITS); + if (year >= 1000) { + return year; + } else if (year >= 50) { + return year + 1900; + } else { + return year + 2000; + } + } + + @Override + int parseHour() throws ParseException { + return parseAsciiDigits(1, 2); + } + + @Override + int parseMinute() throws ParseException { + return parseAsciiDigits(1, 2); + } + + @Override + int parseSecond() throws ParseException { + return parseAsciiDigits(1, 2); + } + + @Override + void parseFwsBetweenTimeOfDayAndZone() throws ParseException { + skipFoldingWhiteSpace(); + } + + @Override + int parseZone() throws ParseException { + try { + if (pos.getIndex() >= text.length()) { + throw new ParseException("Missing zone", pos.getIndex()); + } + + if (peekChar('+') || peekChar('-')) { + return parseZoneOffset(); + } else if (skipAlternativePair('U', 'u', 'T', 't')) { + return 0; + } else if (skipAlternativeTriple('G', 'g', 'M', 'm', + 'T', 't')) { + return 0; + } else { + int hoursOffset; + if (skipAlternative('E', 'e')) { + hoursOffset = 4; + } else if (skipAlternative('C', 'c')) { + hoursOffset = 5; + } else if (skipAlternative('M', 'm')) { + hoursOffset = 6; + } else if (skipAlternative('P', 'p')) { + hoursOffset = 7; + } else { + throw new ParseException("Invalid zone", + pos.getIndex()); + } + if (skipAlternativePair('S', 's', 'T', 't')) { + hoursOffset += 1; + } else if (skipAlternativePair('D', 'd', 'T', 't')) { + } else { + pos.setIndex(pos.getIndex() - 1); + throw new ParseException("Invalid zone", + pos.getIndex()); + } + return hoursOffset * 60; + } + } catch (ParseException e) { + if (logger.isLoggable(Level.FINE)) { + logger.log(Level.FINE, "No timezone? : '" + text + "'", e); + } + + return 0; + } + } + + @Override + boolean isValidZoneOffset(int offset) { + return true; + } + + @Override + boolean skipFoldingWhiteSpace() { + boolean result = peekFoldingWhiteSpace(); + + skipLoop: + while (pos.getIndex() < text.length()) { + switch (text.charAt(pos.getIndex())) { + case ' ': + case '\t': + case '\r': + case '\n': + pos.setIndex(pos.getIndex() + 1); + break; + default: + break skipLoop; + } + } + + return result; + } + + @Override + boolean peekFoldingWhiteSpace() { + return super.peekFoldingWhiteSpace() + || (pos.getIndex() < text.length() + && text.charAt(pos.getIndex()) == '\n'); + } + + } + +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/MimeBodyPart.java b/net-mail/src/main/java/jakarta/mail/internet/MimeBodyPart.java new file mode 100644 index 0000000..5830d1d --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/MimeBodyPart.java @@ -0,0 +1,1719 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import jakarta.activation.DataHandler; +import jakarta.activation.DataSource; +import jakarta.activation.FileDataSource; +import jakarta.mail.BodyPart; +import jakarta.mail.EncodingAware; +import jakarta.mail.FolderClosedException; +import jakarta.mail.Header; +import jakarta.mail.IllegalWriteException; +import jakarta.mail.Message; +import jakarta.mail.MessageRemovedException; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.Part; +import jakarta.mail.util.LineOutputStream; +import jakarta.mail.util.StreamProvider; +import jakarta.mail.util.StreamProvider.EncoderTypes; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + + +/** + * This class represents a MIME body part. It implements the + * BodyPart abstract class and the MimePart + * interface. MimeBodyParts are contained in MimeMultipart + * objects.

+ *

+ * MimeBodyPart uses the InternetHeaders class to parse + * and store the headers of that body part. + * + *


A note on RFC 822 and MIME headers

+ *

+ * RFC 822 header fields must contain only + * US-ASCII characters. MIME allows non ASCII characters to be present + * in certain portions of certain headers, by encoding those characters. + * RFC 2047 specifies the rules for doing this. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the setHeader, addHeader, and + * addHeaderLine methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate. + * + * @author John Mani + * @author Bill Shannon + * @author Kanwar Oberoi + * @see Part + * @see MimePart + * @see MimeUtility + */ + +public class MimeBodyPart extends BodyPart implements MimePart { + + // Paranoia: + // allow this last minute change to be disabled if it causes problems + static final boolean cacheMultipart = // accessed by MimeMessage + MimeUtility.getBooleanSystemProperty("mail.mime.cachemultipart", true); + // Paranoia: + // allow this last minute change to be disabled if it causes problems + private static final boolean setDefaultTextCharset = + MimeUtility.getBooleanSystemProperty( + "mail.mime.setdefaulttextcharset", true); + private static final boolean setContentTypeFileName = + MimeUtility.getBooleanSystemProperty( + "mail.mime.setcontenttypefilename", true); + private static final boolean encodeFileName = + MimeUtility.getBooleanSystemProperty("mail.mime.encodefilename", false); + private static final boolean decodeFileName = + MimeUtility.getBooleanSystemProperty("mail.mime.decodefilename", false); + private static final boolean ignoreMultipartEncoding = + MimeUtility.getBooleanSystemProperty( + "mail.mime.ignoremultipartencoding", true); + private static final boolean allowutf8 = + MimeUtility.getBooleanSystemProperty("mail.mime.allowutf8", true); + /** + * The DataHandler object representing this Part's content. + */ + protected DataHandler dh; + + /** + * Byte array that holds the bytes of the content of this Part. + */ + protected byte[] content; + + /** + * If the data for this body part was supplied by an + * InputStream that implements the SharedInputStream interface, + * contentStream is another such stream representing + * the content of this body part. In this case, content + * will be null. + * + * @since JavaMail 1.2 + */ + protected InputStream contentStream; + + /** + * The InternetHeaders object that stores all the headers + * of this body part. + */ + protected InternetHeaders headers; + + /** + * If our content is a Multipart of Message object, we save it + * the first time it's created by parsing a stream so that changes + * to the contained objects will not be lost. + *

+ * If this field is not null, it's return by the {@link #getContent} + * method. The {@link #getContent} method sets this field if it + * would return a Multipart or MimeMessage object. This field is + * is cleared by the {@link #setDataHandler} method. + * + * @since JavaMail 1.5 + */ + protected Object cachedContent; + + /** + * An empty MimeBodyPart object is created. + * This body part maybe filled in by a client constructing a multipart + * message. + */ + public MimeBodyPart() { + super(); + headers = new InternetHeaders(); + } + + /** + * Constructs a MimeBodyPart by reading and parsing the data from + * the specified input stream. The parser consumes data till the end + * of the given input stream. The input stream must start at the + * beginning of a valid MIME body part and must terminate at the end + * of that body part.

+ *

+ * Note that the "boundary" string that delimits body parts must + * not be included in the input stream. The intention + * is that the MimeMultipart parser will extract each body part's bytes + * from a multipart stream and feed them into this constructor, without + * the delimiter strings. + * + * @param is the body part Input Stream + * @throws MessagingException for failures + */ + public MimeBodyPart(InputStream is) throws MessagingException { + if (!(is instanceof ByteArrayInputStream) && + !(is instanceof BufferedInputStream) && + !(is instanceof SharedInputStream)) + is = new BufferedInputStream(is); + + headers = new InternetHeaders(is); + + if (is instanceof SharedInputStream) { + SharedInputStream sis = (SharedInputStream) is; + contentStream = sis.newStream(sis.getPosition(), -1); + } else { + try { + content = MimeUtility.getBytes(is); + } catch (IOException ioex) { + throw new MessagingException("Error reading input stream", ioex); + } + } + + } + + /** + * Constructs a MimeBodyPart using the given header and + * content bytes.

+ *

+ * Used by providers. + * + * @param headers The header of this part + * @param content bytes representing the body of this part. + * @throws MessagingException for failures + */ + public MimeBodyPart(InternetHeaders headers, byte[] content) + throws MessagingException { + super(); + this.headers = headers; + this.content = content; + } + + static boolean isMimeType(MimePart part, String mimeType) + throws MessagingException { + // XXX - lots of room for optimization here! + String type = part.getContentType(); + try { + return new ContentType(type).match(mimeType); + } catch (ParseException ex) { + // we only need the type and subtype so throw away the rest + try { + int i = type.indexOf(';'); + if (i > 0) + return new ContentType(type.substring(0, i)).match(mimeType); + } catch (ParseException pex2) { + } + return type.equalsIgnoreCase(mimeType); + } + } + + static void setText(MimePart part, String text, String charset, + String subtype) throws MessagingException { + if (charset == null) { + if (MimeUtility.checkAscii(text) != MimeUtility.ALL_ASCII) + charset = MimeUtility.getDefaultMIMECharset(); + else + charset = "us-ascii"; + } + // XXX - should at least ensure that subtype is an atom + part.setContent(text, "text/" + subtype + "; charset=" + + MimeUtility.quote(charset, HeaderTokenizer.MIME)); + } + + static String getDisposition(MimePart part) throws MessagingException { + String s = part.getHeader("Content-Disposition", null); + + if (s == null) + return null; + + ContentDisposition cd = new ContentDisposition(s); + return cd.getDisposition(); + } + + static void setDisposition(MimePart part, String disposition) + throws MessagingException { + if (disposition == null) + part.removeHeader("Content-Disposition"); + else { + String s = part.getHeader("Content-Disposition", null); + if (s != null) { + /* A Content-Disposition header already exists .. + * + * Override disposition, but attempt to retain + * existing disposition parameters + */ + ContentDisposition cd = new ContentDisposition(s); + cd.setDisposition(disposition); + disposition = cd.toString(); + } + part.setHeader("Content-Disposition", disposition); + } + } + + static String getDescription(MimePart part) + throws MessagingException { + String rawvalue = part.getHeader("Content-Description", null); + + if (rawvalue == null) + return null; + + try { + return MimeUtility.decodeText(MimeUtility.unfold(rawvalue)); + } catch (UnsupportedEncodingException ex) { + return rawvalue; + } + } + + static void + setDescription(MimePart part, String description, String charset) + throws MessagingException { + if (description == null) { + part.removeHeader("Content-Description"); + return; + } + + try { + part.setHeader("Content-Description", MimeUtility.fold(21, + MimeUtility.encodeText(description, charset, null))); + } catch (UnsupportedEncodingException uex) { + throw new MessagingException("Encoding error", uex); + } + } + + static String getFileName(MimePart part) throws MessagingException { + String filename = null; + String s = part.getHeader("Content-Disposition", null); + + if (s != null) { + // Parse the header .. + ContentDisposition cd = new ContentDisposition(s); + filename = cd.getParameter("filename"); + } + if (filename == null) { + // Still no filename ? Try the "name" ContentType parameter + s = part.getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(part, s); + if (s != null) { + try { + ContentType ct = new ContentType(s); + filename = ct.getParameter("name"); + } catch (ParseException pex) { + } // ignore it + } + } + if (decodeFileName && filename != null) { + try { + filename = MimeUtility.decodeText(filename); + } catch (UnsupportedEncodingException ex) { + throw new MessagingException("Can't decode filename", ex); + } + } + return filename; + } + + static void setFileName(MimePart part, String name) + throws MessagingException { + if (encodeFileName && name != null) { + try { + name = MimeUtility.encodeText(name); + } catch (UnsupportedEncodingException ex) { + throw new MessagingException("Can't encode filename", ex); + } + } + + // Set the Content-Disposition "filename" parameter + String s = part.getHeader("Content-Disposition", null); + ContentDisposition cd = + new ContentDisposition(s == null ? Part.ATTACHMENT : s); + // ensure that the filename is encoded if necessary + String charset = MimeUtility.getDefaultMIMECharset(); + ParameterList p = cd.getParameterList(); + if (p == null) { + p = new ParameterList(); + cd.setParameterList(p); + } + if (encodeFileName) + p.setLiteral("filename", name); + else + p.set("filename", name, charset); + part.setHeader("Content-Disposition", cd.toString()); + + /* + * Also attempt to set the Content-Type "name" parameter, + * to satisfy ancient MUAs. XXX - This is not RFC compliant. + */ + if (setContentTypeFileName) { + s = part.getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(part, s); + if (s != null) { + try { + ContentType cType = new ContentType(s); + // ensure that the filename is encoded if necessary + p = cType.getParameterList(); + if (p == null) { + p = new ParameterList(); + cType.setParameterList(p); + } + if (encodeFileName) + p.setLiteral("name", name); + else + p.set("name", name, charset); + part.setHeader("Content-Type", cType.toString()); + } catch (ParseException pex) { + } // ignore it + } + } + } + + static String[] getContentLanguage(MimePart part) + throws MessagingException { + String s = part.getHeader("Content-Language", null); + + if (s == null) + return null; + + // Tokenize the header to obtain the Language-tags (skip comments) + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + List v = new ArrayList<>(); + + HeaderTokenizer.Token tk; + int tkType; + + while (true) { + tk = h.next(); // get a language-tag + tkType = tk.getType(); + if (tkType == HeaderTokenizer.Token.EOF) + break; // done + else if (tkType == HeaderTokenizer.Token.ATOM) + v.add(tk.getValue()); + else // invalid token, skip it. + continue; + } + + if (v.isEmpty()) + return null; + + String[] language = new String[v.size()]; + v.toArray(language); + return language; + } + + static void setContentLanguage(MimePart part, String[] languages) + throws MessagingException { + StringBuilder sb = new StringBuilder(languages[0]); + int len = "Content-Language".length() + 2 + languages[0].length(); + for (int i = 1; i < languages.length; i++) { + sb.append(','); + len++; + if (len > 76) { + sb.append("\r\n\t"); + len = 8; + } + sb.append(languages[i]); + len += languages[i].length(); + } + part.setHeader("Content-Language", sb.toString()); + } + + static String getEncoding(MimePart part) throws MessagingException { + String s = part.getHeader("Content-Transfer-Encoding", null); + if (s == null) { + return null; + } + s = s.trim(); + if (s.isEmpty()) { + return null; + } + // quick check for known values to avoid unnecessary use + // of tokenizer. + if (s.equalsIgnoreCase(EncoderTypes.BIT7_ENCODER.getEncoder()) || + s.equalsIgnoreCase(EncoderTypes.BIT8_ENCODER.getEncoder()) || + s.equalsIgnoreCase(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder()) || + s.equalsIgnoreCase(EncoderTypes.BINARY_ENCODER.getEncoder()) || + s.equalsIgnoreCase(EncoderTypes.BASE_64.getEncoder())) { + return s; + } + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + HeaderTokenizer.Token tk; + int tkType; + for (; ; ) { + tk = h.next(); + tkType = tk.getType(); + if (tkType == HeaderTokenizer.Token.EOF) { + break; + } else if (tkType == HeaderTokenizer.Token.ATOM) { + return tk.getValue(); + } else { + continue; + } + } + return s; + } + + static void setEncoding(MimePart part, String encoding) + throws MessagingException { + part.setHeader("Content-Transfer-Encoding", encoding); + } + + /** + * Restrict the encoding to values allowed for the + * Content-Type of the specified MimePart. Returns + * either the original encoding or null. + */ + static String restrictEncoding(MimePart part, String encoding) + throws MessagingException { + if (!ignoreMultipartEncoding || encoding == null) + return encoding; + + if (encoding.equalsIgnoreCase(EncoderTypes.BIT7_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.BIT8_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.BINARY_ENCODER.getEncoder())) + return encoding; // these encodings are always valid + + String type = part.getContentType(); + if (type == null) + return encoding; + + try { + /* + * multipart and message types aren't allowed to have + * encodings except for the three mentioned above. + * If it's one of these types, ignore the encoding. + */ + ContentType cType = new ContentType(type); + if (cType.match("multipart/*")) + return null; + if (cType.match("message/*") && + !MimeUtility.getBooleanSystemProperty( + "mail.mime.allowencodedmessages", false)) + return null; + } catch (ParseException pex) { + // ignore it + } + return encoding; + } + + static void updateHeaders(MimePart part) throws MessagingException { + DataHandler dh = part.getDataHandler(); + if (dh == null) // Huh ? + return; + + try { + String type = dh.getContentType(); + boolean composite = false; + boolean needCTHeader = part.getHeader("Content-Type") == null; + + ContentType cType = new ContentType(type); + + /* + * If this is a multipart, give sub-parts a chance to + * update their headers. Even though the data for this + * multipart may have come from a stream, one of the + * sub-parts may have been updated. + */ + if (cType.match("multipart/*")) { + // If multipart, recurse + composite = true; + Object o; + if (part instanceof MimeBodyPart) { + MimeBodyPart mbp = (MimeBodyPart) part; + o = mbp.cachedContent != null ? + mbp.cachedContent : dh.getContent(); + } else if (part instanceof MimeMessage) { + MimeMessage msg = (MimeMessage) part; + o = msg.cachedContent != null ? + msg.cachedContent : dh.getContent(); + } else + o = dh.getContent(); + if (o instanceof MimeMultipart) + ((MimeMultipart) o).updateHeaders(); + else + throw new MessagingException("MIME part of type \"" + + type + "\" contains object of type " + + o.getClass().getName() + " instead of MimeMultipart"); + } else if (cType.match("message/rfc822")) { + composite = true; + // XXX - call MimeMessage.updateHeaders()? + } + + /* + * If this is our own MimePartDataHandler, we can't update any + * of the headers. + * + * If this is a MimePartDataHandler coming from another part, + * we need to copy over the content headers from the other part. + * Note that the MimePartDataHandler still refers to the original + * data and the original MimePart. + */ + if (dh instanceof MimePartDataHandler) { + MimePartDataHandler mdh = (MimePartDataHandler) dh; + MimePart mpart = mdh.getPart(); + if (mpart != part) { + if (needCTHeader) + part.setHeader("Content-Type", mpart.getContentType()); + // XXX - can't change the encoding of the data from the + // other part without decoding and reencoding it, so + // we just force it to match the original, but if the + // original has no encoding we'll consider reencoding it + String enc = mpart.getEncoding(); + if (enc != null) { + setEncoding(part, enc); + return; + } + } else + return; + } + + // Content-Transfer-Encoding, but only if we don't + // already have one + if (!composite) { // not allowed on composite parts + if (part.getHeader("Content-Transfer-Encoding") == null) + setEncoding(part, MimeUtility.getEncoding(dh)); + + if (needCTHeader && setDefaultTextCharset && + cType.match("text/*") && + cType.getParameter("charset") == null) { + /* + * Set a default charset for text parts. + * We really should examine the data to determine + * whether or not it's all ASCII, but that's too + * expensive so we make an assumption: If we + * chose 7bit encoding for this data, it's probably + * ASCII. (MimeUtility.getEncoding will choose + * 7bit only in this case, but someone might've + * set the Content-Transfer-Encoding header manually.) + */ + String charset; + String enc = part.getEncoding(); + if (enc != null && enc.equalsIgnoreCase(EncoderTypes.BIT7_ENCODER.getEncoder())) + charset = "us-ascii"; + else + charset = MimeUtility.getDefaultMIMECharset(); + cType.setParameter("charset", charset); + type = cType.toString(); + } + } + + // Now, let's update our own headers ... + + // Content-type, but only if we don't already have one + if (needCTHeader) { + /* + * Pull out "filename" from Content-Disposition, and + * use that to set the "name" parameter. This is to + * satisfy older MUAs (DtMail, Roam and probably + * a bunch of others). + */ + if (setContentTypeFileName) { + String s = part.getHeader("Content-Disposition", null); + if (s != null) { + // Parse the header .. + ContentDisposition cd = new ContentDisposition(s); + String filename = cd.getParameter("filename"); + if (filename != null) { + ParameterList p = cType.getParameterList(); + if (p == null) { + p = new ParameterList(); + cType.setParameterList(p); + } + if (encodeFileName) + p.setLiteral("name", + MimeUtility.encodeText(filename)); + else + p.set("name", filename, + MimeUtility.getDefaultMIMECharset()); + type = cType.toString(); + } + } + } + + part.setHeader("Content-Type", type); + } + } catch (IOException ex) { + throw new MessagingException("IOException updating headers", ex); + } + } + + static void invalidateContentHeaders(MimePart part) + throws MessagingException { + part.removeHeader("Content-Type"); + part.removeHeader("Content-Transfer-Encoding"); + } + + static void writeTo(MimePart part, OutputStream os, String[] ignoreList) + throws IOException, MessagingException { + + // see if we already have a LOS + LineOutputStream los = null; + if (os instanceof LineOutputStream) { + los = (LineOutputStream) os; + } else { + Map params = new HashMap<>(); + params.put("allowutf8", allowutf8); + los = StreamProvider.provider().outputLineStream(os, allowutf8); + } + + // First, write out the header + Enumeration hdrLines + = part.getNonMatchingHeaderLines(ignoreList); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + + // Finally, the content. Encode if required. + // XXX: May need to account for ESMTP ? + InputStream is = null; + byte[] buf = null; + try { + /* + * If the data for this part comes from a stream, + * and is already encoded, + * just copy it to the output stream without decoding + * and reencoding it. + */ + DataHandler dh = part.getDataHandler(); + if (dh instanceof MimePartDataHandler) { + MimePartDataHandler mpdh = (MimePartDataHandler) dh; + MimePart mpart = mpdh.getPart(); + if (mpart.getEncoding() != null) + is = mpdh.getContentStream(); + } + if (is != null) { + // now copy the data to the output stream + buf = new byte[8192]; + int len; + while ((len = is.read(buf)) > 0) + os.write(buf, 0, len); + } else { + os = MimeUtility.encode(os, + restrictEncoding(part, part.getEncoding())); + part.getDataHandler().writeTo(os); + } + } finally { + if (is != null) + is.close(); + buf = null; + } + os.flush(); // Needed to complete encoding + } + + /** + * Return the size of the content of this body part in bytes. + * Return -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.

+ *

+ * This implementation returns the size of the content + * array (if not null), or, if contentStream is not + * null, and the available method returns a positive + * number, it returns that number as the size. Otherwise, it returns + * -1. + * + * @return size in bytes, or -1 if not known + */ + @Override + public int getSize() throws MessagingException { + if (content != null) + return content.length; + if (contentStream != null) { + try { + int size = contentStream.available(); + // only believe the size if it's greate than zero, since zero + // is the default returned by the InputStream class itself + if (size > 0) + return size; + } catch (IOException ex) { + // ignore it + } + } + return -1; + } + + /** + * Return the number of lines for the content of this Part. + * Return -1 if this number cannot be determined.

+ *

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

+ *

+ * This implementation returns -1. + * + * @return number of lines, or -1 if not known + */ + @Override + public int getLineCount() throws MessagingException { + return -1; + } + + /** + * Returns the value of the RFC 822 "Content-Type" header field. + * This represents the content type of the content of this + * body part. This value must not be null. If this field is + * unavailable, "text/plain" should be returned.

+ *

+ * This implementation uses getHeader(name) + * to obtain the requisite header field. + * + * @return Content-Type of this body part + */ + @Override + public String getContentType() throws MessagingException { + String s = getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(this, s); + if (s == null) + s = "text/plain"; + return s; + } + + /** + * Is this Part of the specified MIME type? This method + * compares only the primaryType and + * subType. + * The parameters of the content types are ignored.

+ *

+ * For example, this method will return true when + * comparing a Part of content type "text/plain" + * with "text/plain; charset=foobar".

+ *

+ * If the subType of mimeType is the + * special character '*', then the subtype is ignored during the + * comparison. + * + * @throws MessagingException for failures + */ + @Override + public boolean isMimeType(String mimeType) throws MessagingException { + return isMimeType(this, mimeType); + } + + /** + * Returns the disposition from the "Content-Disposition" header field. + * This represents the disposition of this part. The disposition + * describes how the part should be presented to the user.

+ *

+ * If the Content-Disposition field is unavailable, + * null is returned.

+ *

+ * This implementation uses getHeader(name) + * to obtain the requisite header field. + * + * @throws MessagingException for failures + * @see #headers + */ + @Override + public String getDisposition() throws MessagingException { + return getDisposition(this); + } + + /** + * Set the disposition in the "Content-Disposition" header field + * of this body part. If the disposition is null, any existing + * "Content-Disposition" header field is removed. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setDisposition(String disposition) throws MessagingException { + setDisposition(this, disposition); + } + + /** + * Returns the content transfer encoding from the + * "Content-Transfer-Encoding" header + * field. Returns null if the header is unavailable + * or its value is absent.

+ *

+ * This implementation uses getHeader(name) + * to obtain the requisite header field. + * + * @see #headers + */ + @Override + public String getEncoding() throws MessagingException { + return getEncoding(this); + } + + /** + * Returns the value of the "Content-ID" header field. Returns + * null if the field is unavailable or its value is + * absent.

+ *

+ * This implementation uses getHeader(name) + * to obtain the requisite header field. + */ + @Override + public String getContentID() throws MessagingException { + return getHeader("Content-Id", null); + } + + /** + * Set the "Content-ID" header field of this body part. + * If the cid parameter is null, any existing + * "Content-ID" is removed. + * + * @param cid the Content-ID + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + * @since JavaMail 1.3 + */ + public void setContentID(String cid) throws MessagingException { + if (cid == null) + removeHeader("Content-ID"); + else + setHeader("Content-ID", cid); + } + + /** + * Return the value of the "Content-MD5" header field. Returns + * null if this field is unavailable or its value + * is absent.

+ *

+ * This implementation uses getHeader(name) + * to obtain the requisite header field. + */ + @Override + public String getContentMD5() throws MessagingException { + return getHeader("Content-MD5", null); + } + + /** + * Set the "Content-MD5" header field of this body part. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setContentMD5(String md5) throws MessagingException { + setHeader("Content-MD5", md5); + } + + /** + * Get the languages specified in the Content-Language header + * of this MimePart. The Content-Language header is defined by + * RFC 1766. Returns null if this header is not + * available or its value is absent.

+ *

+ * This implementation uses getHeader(name) + * to obtain the requisite header field. + */ + @Override + public String[] getContentLanguage() throws MessagingException { + return getContentLanguage(this); + } + + /** + * Set the Content-Language header of this MimePart. The + * Content-Language header is defined by RFC 1766. + * + * @param languages array of language tags + */ + @Override + public void setContentLanguage(String[] languages) + throws MessagingException { + setContentLanguage(this, languages); + } + + /** + * Returns the "Content-Description" header field of this body part. + * This typically associates some descriptive information with + * this part. Returns null if this field is unavailable or its + * value is absent.

+ *

+ * If the Content-Description field is encoded as per RFC 2047, + * it is decoded and converted into Unicode. If the decoding or + * conversion fails, the raw data is returned as is.

+ *

+ * This implementation uses getHeader(name) + * to obtain the requisite header field. + * + * @return content description + */ + @Override + public String getDescription() throws MessagingException { + return getDescription(this); + } + + /** + * Set the "Content-Description" header field for this body part. + * If the description parameter is null, then any + * existing "Content-Description" fields are removed.

+ *

+ * If the description contains non US-ASCII characters, it will + * be encoded using the platform's default charset. If the + * description contains only US-ASCII characters, no encoding + * is done and it is used as is.

+ *

+ * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description content description + * @throws MessagingException otherwise; an + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setDescription(String description) throws MessagingException { + setDescription(description, null); + } + + /** + * Set the "Content-Description" header field for this body part. + * If the description parameter is null, then any + * existing "Content-Description" fields are removed.

+ *

+ * If the description contains non US-ASCII characters, it will + * be encoded using the specified charset. If the description + * contains only US-ASCII characters, no encoding is done and + * it is used as is.

+ *

+ * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description Description + * @param charset Charset for encoding + * @throws MessagingException otherwise; an + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + public void setDescription(String description, String charset) + throws MessagingException { + setDescription(this, description, charset); + } + + /** + * Get the filename associated with this body part.

+ *

+ * Returns the value of the "filename" parameter from the + * "Content-Disposition" header field of this body part. If its + * not available, returns the value of the "name" parameter from + * the "Content-Type" header field of this body part. + * Returns null if both are absent.

+ *

+ * If the mail.mime.decodefilename System property + * is set to true, the {@link MimeUtility#decodeText + * MimeUtility.decodeText} method will be used to decode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @return filename + */ + @Override + public String getFileName() throws MessagingException { + return getFileName(this); + } + + /** + * Set the filename associated with this body part, if possible.

+ *

+ * Sets the "filename" parameter of the "Content-Disposition" + * header field of this body part. For compatibility with older + * mailers, the "name" parameter of the "Content-Type" header is + * also set.

+ *

+ * If the mail.mime.encodefilename System property + * is set to true, the {@link MimeUtility#encodeText + * MimeUtility.encodeText} method will be used to encode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @param filename the file name + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setFileName(String filename) throws MessagingException { + setFileName(this, filename); + } + + /** + * Return a decoded input stream for this body part's "content".

+ *

+ * This implementation obtains the input stream from the DataHandler. + * That is, it invokes getDataHandler().getInputStream(); + * + * @return an InputStream + * @throws IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * jakarta.activation.DataHandler for more details. + * @throws MessagingException for other failures + * @see #getContentStream + * @see jakarta.activation.DataHandler#getInputStream + */ + @Override + public InputStream getInputStream() + throws IOException, MessagingException { + return getDataHandler().getInputStream(); + } + + /** + * Produce the raw bytes of the content. This method is used + * when creating a DataHandler object for the content. Subclasses + * that can provide a separate input stream for just the Part + * content might want to override this method. + * + * @return an InputStream containing the raw bytes + * @throws MessagingException for failures + * @see #content + * @see MimeMessage#getContentStream + */ + protected InputStream getContentStream() throws MessagingException { + if (contentStream != null) + return ((SharedInputStream) contentStream).newStream(0, -1); + if (content != null) + return new ByteArrayInputStream(content); + + throw new MessagingException("No MimeBodyPart content"); + } + + /** + * Return an InputStream to the raw data with any Content-Transfer-Encoding + * intact. This method is useful if the "Content-Transfer-Encoding" + * header is incorrect or corrupt, which would prevent the + * getInputStream method or getContent method + * from returning the correct data. In such a case the application may + * use this method and attempt to decode the raw data itself.

+ *

+ * This implementation simply calls the getContentStream + * method. + * + * @return an InputStream containing the raw bytes + * @throws MessagingException for failures + * @see #getInputStream + * @see #getContentStream + * @since JavaMail 1.2 + */ + public InputStream getRawInputStream() throws MessagingException { + return getContentStream(); + } + + /** + * Return a DataHandler for this body part's content.

+ *

+ * The implementation provided here works just like the + * the implementation in MimeMessage. + * + * @see MimeMessage#getDataHandler + */ + @Override + public DataHandler getDataHandler() throws MessagingException { + if (dh == null) + dh = new MimePartDataHandler(this); + return dh; + } + + /** + * This method provides the mechanism to set this body part's content. + * The given DataHandler object should wrap the actual content. + * + * @param dh The DataHandler for the content + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setDataHandler(DataHandler dh) + throws MessagingException { + this.dh = dh; + cachedContent = null; + MimeBodyPart.invalidateContentHeaders(this); + } + + /** + * Return the content as a Java object. The type of the object + * returned is of course dependent on the content itself. For + * example, the native format of a text/plain content is usually + * a String object. The native format for a "multipart" + * content is always a Multipart subclass. For content types that are + * unknown to the DataHandler system, an input stream is returned + * as the content.

+ *

+ * This implementation obtains the content from the DataHandler. + * That is, it invokes getDataHandler().getContent(); + * If the content is a Multipart or Message object and was created by + * parsing a stream, the object is cached and returned in subsequent + * calls so that modifications to the content will not be lost. + * + * @return Object + * @throws IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * jakarta.activation.DataHandler for more details. + * @throws MessagingException for other failures + */ + @Override + public Object getContent() throws IOException, MessagingException { + if (cachedContent != null) + return cachedContent; + Object c; + try { + c = getDataHandler().getContent(); + } catch (IOException e) { + if (e.getCause() instanceof FolderClosedException) { + FolderClosedException fce = (FolderClosedException) e.getCause(); + throw new FolderClosedException(fce.getFolder(), e.getMessage()); + } else if (e.getCause() instanceof MessagingException) { + throw new MessageRemovedException(e.getMessage()); + } else { + throw e; + } + } + if (cacheMultipart && + (c instanceof Multipart || c instanceof Message) && + (content != null || contentStream != null)) { + cachedContent = c; + /* + * We may abandon the input stream so make sure + * the MimeMultipart has consumed the stream. + */ + if (c instanceof MimeMultipart) + ((MimeMultipart) c).parse(); + } + return c; + } + + /** + * This method sets the body part's content to a Multipart object. + * + * @param mp The multipart object that is the Message's content + * @throws IllegalWriteException if the underlying + * implementation does not support modification of + * existing values. + * @throws IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setContent(Multipart mp) throws MessagingException { + setDataHandler(new DataHandler(mp, mp.getContentType())); + mp.setParent(this); + } + + /** + * A convenience method for setting this body part's content.

+ *

+ * The content is wrapped in a DataHandler object. Note that a + * DataContentHandler class for the specified type should be + * available to the Jakarta Mail implementation for this to work right. + * That is, to do setContent(foobar, "application/x-foobar"), + * a DataContentHandler for "application/x-foobar" should be installed. + * Refer to the Java Activation Framework for more information. + * + * @param o the content object + * @param type Mime type of the object + * @throws IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @throws IllegalStateException if this body part is + * obtained from a READ_ONLY folder. + */ + @Override + public void setContent(Object o, String type) + throws MessagingException { + if (o instanceof Multipart) { + setContent((Multipart) o); + } else { + setDataHandler(new DataHandler(o, type)); + } + } + + /** + * Convenience method that sets the given String as this + * part's content, with a MIME type of "text/plain". If the + * string contains non US-ASCII characters, it will be encoded + * using the platform's default charset. The charset is also + * used to set the "charset" parameter.

+ *

+ * Note that there may be a performance penalty if + * text is large, since this method may have + * to scan all the characters to determine what charset to + * use.

+ *

+ * If the charset is already known, use the + * setText method that takes the charset parameter. + * + * @param text the text content to set + * @throws MessagingException if an error occurs + * @see #setText(String text, String charset) + */ + @Override + public void setText(String text) throws MessagingException { + setText(text, null); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a MIME type of "text/plain" and the specified + * charset. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @throws MessagingException if an error occurs + */ + @Override + public void setText(String text, String charset) + throws MessagingException { + setText(this, text, charset, "plain"); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a primary MIME type of "text" and the specified + * MIME subtype. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @param subtype the MIME subtype to use (e.g., "html") + * @throws MessagingException if an error occurs + * @since JavaMail 1.4 + */ + @Override + public void setText(String text, String charset, String subtype) + throws MessagingException { + setText(this, text, charset, subtype); + } + + /** + * Use the specified file to provide the data for this part. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The encoding will be chosen appropriately for the + * file data. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the File object to attach + * @throws IOException errors related to accessing the file + * @throws MessagingException message related errors + * @since JavaMail 1.4 + */ + public void attachFile(File file) throws IOException, MessagingException { + FileDataSource fds = new FileDataSource(file); + this.setDataHandler(new DataHandler(fds)); + this.setFileName(fds.getName()); + this.setDisposition(ATTACHMENT); + } + + /** + * Use the specified file to provide the data for this part. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The encoding will be chosen appropriately for the + * file data. + * + * @param file the name of the file to attach + * @throws IOException errors related to accessing the file + * @throws MessagingException message related errors + * @since JavaMail 1.4 + */ + public void attachFile(String file) throws IOException, MessagingException { + File f = new File(file); + attachFile(f); + } + + /** + * Use the specified file with the specified Content-Type and + * Content-Transfer-Encoding to provide the data for this part. + * If contentType or encoding are null, appropriate values will + * be chosen. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the File object to attach + * @param contentType the Content-Type, or null + * @param encoding the Content-Transfer-Encoding, or null + * @throws IOException errors related to accessing the file + * @throws MessagingException message related errors + * @since JavaMail 1.5 + */ + public void attachFile(File file, String contentType, String encoding) + throws IOException, MessagingException { + DataSource fds = new EncodedFileDataSource(file, contentType, encoding); + this.setDataHandler(new DataHandler(fds)); + this.setFileName(fds.getName()); + this.setDisposition(ATTACHMENT); + } + + /** + * Use the specified file with the specified Content-Type and + * Content-Transfer-Encoding to provide the data for this part. + * If contentType or encoding are null, appropriate values will + * be chosen. + * The simple file name is used as the file name for this + * part and the data in the file is used as the data for this + * part. The disposition of this part is set to + * {@link Part#ATTACHMENT Part.ATTACHMENT}. + * + * @param file the name of the file + * @param contentType the Content-Type, or null + * @param encoding the Content-Transfer-Encoding, or null + * @throws IOException errors related to accessing the file + * @throws MessagingException message related errors + * @since JavaMail 1.5 + */ + public void attachFile(String file, String contentType, String encoding) + throws IOException, MessagingException { + attachFile(new File(file), contentType, encoding); + } + + /** + * Save the contents of this part in the specified file. The content + * is decoded and saved, without any of the MIME headers. + * + * @param file the File object to write to + * @throws IOException errors related to accessing the file + * @throws MessagingException message related errors + * @since JavaMail 1.4 + */ + public void saveFile(File file) throws IOException, MessagingException { + Files.copy(getInputStream(), file.toPath(), StandardCopyOption.REPLACE_EXISTING); + } + + ///////////////////////////////////////////////////////////// + // Package private convenience methods to share code among // + // MimeMessage and MimeBodyPart // + ///////////////////////////////////////////////////////////// + + /** + * Save the contents of this part in the specified file. The content + * is decoded and saved, without any of the MIME headers. + * + * @param file the name of the file to write to + * @throws IOException errors related to accessing the file + * @throws MessagingException message related errors + * @since JavaMail 1.4 + */ + public void saveFile(String file) throws IOException, MessagingException { + File f = new File(file); + saveFile(f); + } + + /** + * Output the body part as an RFC 822 format stream. + * + * @throws IOException if an error occurs writing to the + * stream or if an error is generated + * by the jakarta.activation layer. + * @throws MessagingException for other failures + * @see jakarta.activation.DataHandler#writeTo + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + writeTo(this, os, null); + } + + /** + * 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 + * @see MimeUtility + */ + @Override + public String[] getHeader(String name) throws MessagingException { + 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 fields in returned string + * @return the value fields for all headers with + * this name + * @throws MessagingException for failures + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + return headers.getHeader(name, delimiter); + } + + /** + * Set the value for this header_name. Replaces all existing + * header values with this new value. Note that RFC 822 headers + * must contain only US-ASCII characters, so a header that + * contains non US-ASCII characters must be encoded as per the + * rules of RFC 2047. + * + * @param name header name + * @param value header value + * @see MimeUtility + */ + @Override + public void setHeader(String name, String value) + throws MessagingException { + headers.setHeader(name, value); + } + + /** + * Add this value to the existing values for this header_name. + * Note that RFC 822 headers must contain only US-ASCII + * characters, so a header that contains non US-ASCII characters + * must be encoded as per the rules of RFC 2047. + * + * @param name header name + * @param value header value + * @see MimeUtility + */ + @Override + public void addHeader(String name, String value) + throws MessagingException { + headers.addHeader(name, value); + } + + /** + * Remove all headers with this name. + */ + @Override + public void removeHeader(String name) throws MessagingException { + headers.removeHeader(name); + } + + /** + * Return all the headers from this Message as an Enumeration of + * Header objects. + */ + @Override + public Enumeration

getAllHeaders() throws MessagingException { + return headers.getAllHeaders(); + } + + /** + * Return matching headers from this Message as an Enumeration of + * Header objects. + */ + @Override + public Enumeration
getMatchingHeaders(String[] names) + throws MessagingException { + return headers.getMatchingHeaders(names); + } + + /** + * Return non-matching headers from this Message as an + * Enumeration of Header objects. + */ + @Override + public Enumeration
getNonMatchingHeaders(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaders(names); + } + + /** + * Add a header line to this body part + */ + @Override + public void addHeaderLine(String line) throws MessagingException { + headers.addHeaderLine(line); + } + + /** + * Get all header lines as an Enumeration of Strings. A Header + * line is a raw RFC 822 header line, containing both the "name" + * and "value" field. + */ + @Override + public Enumeration getAllHeaderLines() throws MessagingException { + return headers.getAllHeaderLines(); + } + + /** + * Get matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header line, containing both + * the "name" and "value" field. + */ + @Override + public Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getMatchingHeaderLines(names); + } + + /** + * Get non-matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header line, containing both + * the "name" and "value" field. + */ + @Override + public Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaderLines(names); + } + + /** + * Examine the content of this body part and update the appropriate + * MIME headers. Typical headers that get set here are + * Content-Type and Content-Transfer-Encoding. + * Headers might need to be updated in two cases: + * + *
+ * - A message being crafted by a mail application will certainly + * need to activate this method at some point to fill up its internal + * headers. + * + *
+ * - A message read in from a Store will have obtained + * all its headers from the store, and so doesn't need this. + * However, if this message is editable and if any edits have + * been made to either the content or message structure, we might + * need to resync our headers. + * + *
+ * In both cases this method is typically called by the + * Message.saveChanges method.

+ *

+ * If the {@link #cachedContent} field is not null (that is, + * it references a Multipart or Message object), then + * that object is used to set a new DataHandler, any + * stream data used to create this object is discarded, + * and the {@link #cachedContent} field is cleared. + * + * @throws MessagingException for failures + */ + protected void updateHeaders() throws MessagingException { + updateHeaders(this); + /* + * If we've cached a Multipart or Message object then + * we're now committed to using this instance of the + * object and we discard any stream data used to create + * this object. + */ + if (cachedContent != null) { + dh = new DataHandler(cachedContent, getContentType()); + cachedContent = null; + content = null; + if (contentStream != null) { + try { + contentStream.close(); + } catch (IOException ioex) { + } // nothing to do + } + contentStream = null; + } + } + + /** + * A FileDataSource class that allows us to specify the + * Content-Type and Content-Transfer-Encoding. + */ + private static class EncodedFileDataSource extends FileDataSource + implements EncodingAware { + private String contentType; + private String encoding; + + public EncodedFileDataSource(File file, String contentType, + String encoding) { + super(file); + this.contentType = contentType; + this.encoding = encoding; + } + + // overrides DataSource.getContentType() + @Override + public String getContentType() { + return contentType != null ? contentType : super.getContentType(); + } + + // implements EncodingAware.getEncoding() + @Override + public String getEncoding() { + return encoding; + } + } + + /** + * A special DataHandler used only as a marker to indicate that + * the source of the data is a MimePart (that is, a byte array + * or a stream). This prevents updateHeaders from trying to + * change the headers for such data. In particular, the original + * Content-Transfer-Encoding for the data must be preserved. + * Otherwise the data would need to be decoded and reencoded. + */ + static class MimePartDataHandler extends DataHandler { + MimePart part; + + public MimePartDataHandler(MimePart part) { + super(new MimePartDataSource(part)); + this.part = part; + } + + InputStream getContentStream() throws MessagingException { + InputStream is = null; + + if (part instanceof MimeBodyPart) { + MimeBodyPart mbp = (MimeBodyPart) part; + is = mbp.getContentStream(); + } else if (part instanceof MimeMessage) { + MimeMessage msg = (MimeMessage) part; + is = msg.getContentStream(); + } + return is; + } + + MimePart getPart() { + return part; + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/MimeMessage.java b/net-mail/src/main/java/jakarta/mail/internet/MimeMessage.java new file mode 100644 index 0000000..4182bb9 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/MimeMessage.java @@ -0,0 +1,2326 @@ +/* + * Copyright (c) 1997, 2024 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 jakarta.mail.internet; + +import jakarta.activation.DataHandler; +import jakarta.mail.Address; +import jakarta.mail.Flags; +import jakarta.mail.Folder; +import jakarta.mail.FolderClosedException; +import jakarta.mail.Header; +import jakarta.mail.IllegalWriteException; +import jakarta.mail.Message; +import jakarta.mail.MessageRemovedException; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.Session; +import jakarta.mail.util.LineOutputStream; +import jakarta.mail.util.StreamProvider; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; +import java.util.Properties; +import java.util.ServiceConfigurationError; + + +/** + * This class represents a MIME style email message. It implements + * the Message abstract class and the MimePart + * interface.

+ *

+ * Clients wanting to create new MIME style messages will instantiate + * an empty MimeMessage object and then fill it with appropriate + * attributes and content.

+ *

+ * Service providers that implement MIME compliant backend stores may + * want to subclass MimeMessage and override certain methods to provide + * specific implementations. The simplest case is probably a provider + * that generates a MIME style input stream and leaves the parsing of + * the stream to this class.

+ *

+ * MimeMessage uses the InternetHeaders class to parse and + * store the top level RFC 822 headers of a message.

+ *

+ * The mail.mime.address.strict session property controls + * the parsing of address headers. By default, strict parsing of address + * headers is done. If this property is set to "false", + * strict parsing is not done and many illegal addresses that sometimes + * occur in real messages are allowed. See the InternetAddress + * class for details. + * + *


A note on RFC 822 and MIME headers

+ *

+ * RFC 822 header fields must contain only + * US-ASCII characters. MIME allows non ASCII characters to be present + * in certain portions of certain headers, by encoding those characters. + * RFC 2047 specifies the rules for doing this. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the setHeader, addHeader, and + * addHeaderLine methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate. + * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + * @author Kanwar Oberoi + * @see MimeUtility + * @see jakarta.mail.Part + * @see Message + * @see MimePart + * @see InternetAddress + */ +@SuppressWarnings("serial") +public class MimeMessage extends Message implements MimePart { + + // Used to parse dates + private static final MailDateFormat mailDateFormat = new MailDateFormat(); + // used above in reply() + private static final Flags answeredFlag = new Flags(Flags.Flag.ANSWERED); + /** + * The DataHandler object representing this Message's content. + */ + protected DataHandler dh; + /** + * Byte array that holds the bytes of this Message's content. + */ + protected byte[] content; + /** + * If the data for this message was supplied by an + * InputStream that implements the SharedInputStream interface, + * contentStream is another such stream representing + * the content of this message. In this case, content + * will be null. + * + * @since JavaMail 1.2 + */ + protected InputStream contentStream; + /** + * The InternetHeaders object that stores the header + * of this message. + */ + protected InternetHeaders headers; + /** + * The Flags for this message. + */ + protected Flags flags; + /** + * A flag indicating whether the message has been modified. + * If the message has not been modified, any data in the + * content array is assumed to be valid and is used + * directly in the writeTo method. This flag is + * set to true when an empty message is created or when the + * saveChanges method is called. + * + * @since JavaMail 1.2 + */ + protected boolean modified = false; + /** + * Does the saveChanges method need to be called on + * this message? This flag is set to false by the public constructor + * and set to true by the saveChanges method. The + * writeTo method checks this flag and calls the + * saveChanges method as necessary. This avoids the + * common mistake of forgetting to call the saveChanges + * method on a newly constructed message. + * + * @since JavaMail 1.2 + */ + protected boolean saved = false; + /** + * If our content is a Multipart or Message object, we save it + * the first time it's created by parsing a stream so that changes + * to the contained objects will not be lost.

+ *

+ * If this field is not null, it's return by the {@link #getContent} + * method. The {@link #getContent} method sets this field if it + * would return a Multipart or MimeMessage object. This field is + * is cleared by the {@link #setDataHandler} method. + * + * @since JavaMail 1.5 + */ + protected Object cachedContent; + // Should addresses in headers be parsed in "strict" mode? + private boolean strict = true; + // Is UTF-8 allowed in headers? + private boolean allowutf8 = false; + + /** + * 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 Sesssion + */ + public MimeMessage(Session session) { + super(session); + modified = true; + headers = new InternetHeaders(); + flags = new Flags(); // empty flags object + initStrict(); + } + + /** + * Constructs a MimeMessage 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.

+ *

+ * The input stream contains an entire MIME formatted message with + * headers and data. + * + * @param session Session object for this message + * @param is the message input stream + * @throws MessagingException for failures + */ + @SuppressWarnings("this-escape") + public MimeMessage(Session session, InputStream is) + throws MessagingException { + super(session); + flags = new Flags(); // empty Flags object + initStrict(); + parse(is); + saved = true; + } + + /** + * Constructs a new MimeMessage 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 + * @throws MessagingException for failures + * @since JavaMail 1.2 + */ + @SuppressWarnings("this-escape") + public MimeMessage(MimeMessage source) throws MessagingException { + super(source.session); + flags = source.getFlags(); + if (flags == null) // make sure flags is always set + flags = new Flags(); + ByteArrayOutputStream bos; + int size = source.getSize(); + if (size > 0) + bos = new ByteArrayOutputStream(size); + else + bos = new ByteArrayOutputStream(); + try { + strict = source.strict; + source.writeTo(bos); + bos.close(); + try (InputStream bis = provider().inputSharedByteArray(bos.toByteArray())) { + parse(bis); + } + saved = true; + } catch (IOException ex) { + // should never happen, but just in case... + throw new MessagingException("IOException while copying message", + ex); + } + } + + /** + * Constructs an empty MimeMessage object with the given Folder + * and message number.

+ *

+ * This method is for providers subclassing MimeMessage. + * + * @param folder the Folder this message is from + * @param msgnum the number of this message + */ + protected MimeMessage(Folder folder, int msgnum) { + super(folder, msgnum); + flags = new Flags(); // empty Flags object + saved = true; + initStrict(); + } + + /** + * Constructs a MimeMessage 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.

+ *

+ * This method is for providers subclassing MimeMessage. + * + * @param folder The containing folder. + * @param is the message input stream + * @param msgnum Message number of this message within its folder + * @throws MessagingException for failures + */ + @SuppressWarnings("this-escape") + protected MimeMessage(Folder folder, InputStream is, int msgnum) + throws MessagingException { + this(folder, msgnum); + initStrict(); + parse(is); + } + + /** + * Constructs a MimeMessage from the given InternetHeaders object + * and content. + *

+ * This method is for providers subclassing MimeMessage. + * + * @param folder The containing folder. + * @param headers The headers + * @param content The message content + * @param msgnum Message number of this message within its folder + * @throws MessagingException for failures + */ + protected MimeMessage(Folder folder, InternetHeaders headers, + byte[] content, int msgnum) throws MessagingException { + this(folder, msgnum); + this.headers = headers; + this.content = content; + initStrict(); + } + + /** + * Set the strict flag based on property. + */ + private void initStrict() { + if (session != null) { + Properties props = session.getProperties(); + strict = MimeUtility.getBooleanProperty(props, "mail.mime.address.strict", true); + allowutf8 = MimeUtility.getBooleanProperty(props, "mail.mime.allowutf8", false); + } + } + + /** + * Parse the InputStream setting the headers and + * content fields appropriately. Also resets the + * modified flag.

+ *

+ * This method is intended for use by subclasses that need to + * control when the InputStream is parsed. + * + * @param is The message input stream + * @throws MessagingException for failures + */ + protected void parse(InputStream is) throws MessagingException { + + if (!(is instanceof ByteArrayInputStream) && + !(is instanceof BufferedInputStream) && + !(is instanceof SharedInputStream)) + is = new BufferedInputStream(is); + + headers = createInternetHeaders(is); + + if (is instanceof SharedInputStream) { + SharedInputStream sis = (SharedInputStream) is; + contentStream = sis.newStream(sis.getPosition(), -1); + } else { + try { + content = MimeUtility.getBytes(is); + } catch (IOException ioex) { + throw new MessagingException("IOException", ioex); + } + } + + modified = false; + } + + /** + * Returns the value of the RFC 822 "From" header fields. If this + * header field is absent, the "Sender" header field is used. + * If the "Sender" header field is also absent, null + * is returned.

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return Address object + * @throws MessagingException for failures + * @see #headers + */ + @Override + public Address[] getFrom() throws MessagingException { + Address[] a = getAddressHeader("From"); + if (a == null) + a = getAddressHeader("Sender"); + + return a; + } + + /** + * Set the RFC 822 "From" header field. Any existing values are + * replaced with the given address. If address is null, + * this header is removed. + * + * @param address the sender of this message + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setFrom(Address address) throws MessagingException { + if (address == null) + removeHeader("From"); + else + setAddressHeader("From", new Address[]{address}); + } + + /** + * Set the RFC 822 "From" header field. Any existing values are + * replaced with the given addresses. If address is null, + * this header is removed. + * + * @param address the sender(s) of this message + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + * @since JvaMail 1.5 + */ + public void setFrom(String address) throws MessagingException { + if (address == null) + removeHeader("From"); + else + setAddressHeader("From", InternetAddress.parse(address)); + } + + /** + * Set the RFC 822 "From" header field using the value of the + * InternetAddress.getLocalAddress method. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setFrom() throws MessagingException { + InternetAddress me = null; + try { + me = InternetAddress._getLocalAddress(session); + } catch (Exception ex) { + // if anything goes wrong (UnknownHostException), + // chain the exception + throw new MessagingException("No From address", ex); + } + if (me != null) + setFrom(me); + else + throw new MessagingException("No From address"); + } + + /** + * Add the specified addresses to the existing "From" field. If + * the "From" field does not already exist, it is created. + * + * @param addresses the senders of this message + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void addFrom(Address[] addresses) throws MessagingException { + addAddressHeader("From", addresses); + } + + /** + * Returns the value of the RFC 822 "Sender" header field. + * If the "Sender" header field is absent, null + * is returned.

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return Address object + * @throws MessagingException for failures + * @see #headers + * @since JavaMail 1.3 + */ + public Address getSender() throws MessagingException { + Address[] a = getAddressHeader("Sender"); + if (a == null || a.length == 0) + return null; + return a[0]; // there can be only one + } + + /** + * Set the RFC 822 "Sender" header field. Any existing values are + * replaced with the given address. If address is null, + * this header is removed. + * + * @param address the sender of this message + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + * @since JavaMail 1.3 + */ + public void setSender(Address address) throws MessagingException { + if (address == null) + removeHeader("Sender"); + else + setAddressHeader("Sender", new Address[]{address}); + } + + /** + * Returns the recepients specified by the type. The mapping + * between the type and the corresponding RFC 822 header is + * as follows: + *

+     * 		Message.RecipientType.TO		"To"
+     * 		Message.RecipientType.CC		"Cc"
+     * 		Message.RecipientType.BCC		"Bcc"
+     * 		MimeMessage.RecipientType.NEWSGROUPS	"Newsgroups"
+     * 

+ *

+ * Returns null if the header specified by the type is not found + * or if its value is empty.

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @param type Type of recepient + * @return array of Address objects + * @throws MessagingException if header could not + * be retrieved + * @throws AddressException if the header is misformatted + * @see #headers + * @see Message.RecipientType#TO + * @see Message.RecipientType#CC + * @see Message.RecipientType#BCC + * @see RecipientType#NEWSGROUPS + */ + @Override + public Address[] getRecipients(Message.RecipientType type) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + String s = getHeader("Newsgroups", ","); + return (s == null) ? null : NewsAddress.parse(s); + } else + return getAddressHeader(getHeaderName(type)); + } + + /** + * Get all the recipient addresses for the message. + * Extracts the TO, CC, BCC, and NEWSGROUPS recipients. + * + * @return array of Address objects + * @throws MessagingException for failures + * @see Message.RecipientType#TO + * @see Message.RecipientType#CC + * @see Message.RecipientType#BCC + * @see RecipientType#NEWSGROUPS + */ + @Override + public Address[] getAllRecipients() throws MessagingException { + Address[] all = super.getAllRecipients(); + Address[] ng = getRecipients(RecipientType.NEWSGROUPS); + + if (ng == null) + return all; // the common case + if (all == null) + return ng; // a rare case + + Address[] addresses = new Address[all.length + ng.length]; + System.arraycopy(all, 0, addresses, 0, all.length); + System.arraycopy(ng, 0, addresses, all.length, ng.length); + return addresses; + } + + /** + * Set the specified recipient type to the given addresses. + * If the address parameter is null, the corresponding + * recipient field is removed. + * + * @param type Recipient type + * @param addresses Addresses + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + * @see #getRecipients + */ + @Override + public void setRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + if (addresses == null || addresses.length == 0) + removeHeader("Newsgroups"); + else + setHeader("Newsgroups", NewsAddress.toString(addresses)); + } else + setAddressHeader(getHeaderName(type), addresses); + } + + /** + * Set the specified recipient type to the given addresses. + * If the address parameter is null, the corresponding + * recipient field is removed. + * + * @param type Recipient type + * @param addresses Addresses + * @throws AddressException if the attempt to parse the + * addresses String fails + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + * @see #getRecipients + * @since JavaMail 1.2 + */ + public void setRecipients(Message.RecipientType type, String addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + if (addresses == null || addresses.length() == 0) + removeHeader("Newsgroups"); + else + setHeader("Newsgroups", addresses); + } else + setAddressHeader(getHeaderName(type), + addresses == null ? null : InternetAddress.parse(addresses)); + } + + /** + * Add the given addresses to the specified recipient type. + * + * @param type Recipient type + * @param addresses Addresses + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void addRecipients(Message.RecipientType type, Address[] addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + String s = NewsAddress.toString(addresses); + if (s != null) + addHeader("Newsgroups", s); + } else + addAddressHeader(getHeaderName(type), addresses); + } + + /** + * Add the given addresses to the specified recipient type. + * + * @param type Recipient type + * @param addresses Addresses + * @throws AddressException if the attempt to parse the + * addresses String fails + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + * @since JavaMail 1.2 + */ + public void addRecipients(Message.RecipientType type, String addresses) + throws MessagingException { + if (type == RecipientType.NEWSGROUPS) { + if (addresses != null && addresses.length() != 0) + addHeader("Newsgroups", addresses); + } else + addAddressHeader(getHeaderName(type), + InternetAddress.parse(addresses)); + } + + /** + * Return the value of the RFC 822 "Reply-To" header field. If + * this header is unavailable or its value is absent, then + * the getFrom method is called and its value is returned. + *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @throws MessagingException for failures + * @see #headers + */ + @Override + public Address[] getReplyTo() throws MessagingException { + Address[] a = getAddressHeader("Reply-To"); + if (a == null || a.length == 0) + a = getFrom(); + return a; + } + + /** + * Set the RFC 822 "Reply-To" header field. If the address + * parameter is null, this header is removed. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setReplyTo(Address[] addresses) throws MessagingException { + setAddressHeader("Reply-To", addresses); + } + + // Convenience method to get addresses + private Address[] getAddressHeader(String name) + throws MessagingException { + String s = getHeader(name, ","); + return (s == null) ? null : InternetAddress.parseHeader(s, strict); + } + + // Convenience method to set addresses + private void setAddressHeader(String name, Address[] addresses) + throws MessagingException { + String s; + if (allowutf8) + s = InternetAddress.toUnicodeString(addresses, name.length() + 2); + else + s = InternetAddress.toString(addresses, name.length() + 2); + if (s == null) + removeHeader(name); + else + setHeader(name, s); + } + + private void addAddressHeader(String name, Address[] addresses) + throws MessagingException { + if (addresses == null || addresses.length == 0) + return; + Address[] a = getAddressHeader(name); + Address[] anew; + if (a == null || a.length == 0) + anew = addresses; + else { + anew = new Address[a.length + addresses.length]; + System.arraycopy(a, 0, anew, 0, a.length); + System.arraycopy(addresses, 0, anew, a.length, addresses.length); + } + String s; + if (allowutf8) + s = InternetAddress.toUnicodeString(anew, name.length() + 2); + else + s = InternetAddress.toString(anew, name.length() + 2); + if (s == null) + return; + setHeader(name, s); + } + + /** + * Returns the value of the "Subject" header field. Returns null + * if the subject field is unavailable or its value is absent.

+ *

+ * If the subject is encoded as per RFC 2047, it is decoded and + * converted into Unicode. If the decoding or conversion fails, the + * raw data is returned as is.

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return Subject + * @throws MessagingException for failures + * @see #headers + */ + @Override + public String getSubject() throws MessagingException { + String rawvalue = getHeader("Subject", null); + + if (rawvalue == null) + return null; + + try { + return MimeUtility.decodeText(MimeUtility.unfold(rawvalue)); + } catch (UnsupportedEncodingException ex) { + return rawvalue; + } + } + + /** + * Set the "Subject" header field. If the subject contains + * non US-ASCII characters, it will be encoded using the + * platform's default charset. If the subject contains only + * US-ASCII characters, no encoding is done and it is used + * as-is. If the subject is null, the existing "Subject" field + * is removed.

+ *

+ * The application must ensure that the subject does not contain + * any line breaks.

+ *

+ * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param subject The subject + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setSubject(String subject) throws MessagingException { + setSubject(subject, null); + } + + /** + * Set the "Subject" header field. If the subject contains non + * US-ASCII characters, it will be encoded using the specified + * charset. If the subject contains only US-ASCII characters, no + * encoding is done and it is used as-is. If the subject is null, + * the existing "Subject" header field is removed.

+ *

+ * The application must ensure that the subject does not contain + * any line breaks.

+ *

+ * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param subject The subject + * @param charset The charset + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + public void setSubject(String subject, String charset) + throws MessagingException { + if (subject == null) { + removeHeader("Subject"); + } else { + try { + setHeader("Subject", MimeUtility.fold(9, + MimeUtility.encodeText(subject, charset, null))); + } catch (UnsupportedEncodingException uex) { + throw new MessagingException("Encoding error", uex); + } + } + } + + /** + * Returns the value of the RFC 822 "Date" field. This is the date + * on which this message was sent. Returns null if this field is + * unavailable or its value is absent.

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return The sent Date + * @throws MessagingException for failures + */ + @Override + public Date getSentDate() throws MessagingException { + String s = getHeader("Date", null); + if (s != null) { + try { + synchronized (mailDateFormat) { + return mailDateFormat.parse(s); + } + } catch (ParseException pex) { + return null; + } + } + + return null; + } + + /** + * Set the RFC 822 "Date" header field. This is the date on which the + * creator of the message indicates that the message is complete + * and ready for delivery. If the date parameter is + * null, the existing "Date" field is removed. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setSentDate(Date d) throws MessagingException { + if (d == null) + removeHeader("Date"); + else { + synchronized (mailDateFormat) { + setHeader("Date", mailDateFormat.format(d)); + } + } + } + + /** + * Returns the Date on this message was received. Returns + * null if this date cannot be obtained.

+ *

+ * Note that RFC 822 does not define a field for the received + * date. Hence only implementations that can provide this date + * need return a valid value.

+ *

+ * This implementation returns null. + * + * @return the date this message was received + * @throws MessagingException for failures + */ + @Override + public Date getReceivedDate() throws MessagingException { + return null; + } + + /** + * Return the size of the content of this message in bytes. + * Return -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.

+ *

+ * This implementation returns the size of the content + * array (if not null), or, if contentStream is not + * null, and the available method returns a positive + * number, it returns that number as the size. Otherwise, it returns + * -1. + * + * @return size of content in bytes + * @throws MessagingException for failures + */ + @Override + public int getSize() throws MessagingException { + if (content != null) + return content.length; + if (contentStream != null) { + try { + int size = contentStream.available(); + // only believe the size if it's greater than zero, since zero + // is the default returned by the InputStream class itself + if (size > 0) + return size; + } catch (IOException ex) { + // ignore it + } + } + return -1; + } + + /** + * Return the number of lines for the content of this message. + * Return -1 if this number cannot be determined.

+ *

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

+ *

+ * This implementation returns -1. + * + * @return number of lines in the content. + * @throws MessagingException for failures + */ + @Override + public int getLineCount() throws MessagingException { + return -1; + } + + /** + * Returns the value of the RFC 822 "Content-Type" header field. + * This represents the content-type of the content of this + * message. This value must not be null. If this field is + * unavailable, "text/plain" should be returned.

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return The ContentType of this part + * @throws MessagingException for failures + * @see jakarta.activation.DataHandler + */ + @Override + public String getContentType() throws MessagingException { + String s = getHeader("Content-Type", null); + s = MimeUtil.cleanContentType(this, s); + if (s == null) + return "text/plain"; + return s; + } + + /** + * Is this Part of the specified MIME type? This method + * compares only the primaryType and + * subType. + * The parameters of the content types are ignored.

+ *

+ * For example, this method will return true when + * comparing a Part of content type "text/plain" + * with "text/plain; charset=foobar".

+ *

+ * If the subType of mimeType is the + * special character '*', then the subtype is ignored during the + * comparison. + * + * @param mimeType the MIME type to check + * @return true if it matches the MIME type + * @throws MessagingException for failures + */ + @Override + public boolean isMimeType(String mimeType) throws MessagingException { + return MimeBodyPart.isMimeType(this, mimeType); + } + + /** + * Returns the disposition from the "Content-Disposition" header field. + * This represents the disposition of this part. The disposition + * describes how the part should be presented to the user.

+ *

+ * If the Content-Disposition field is unavailable, + * null is returned.

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return disposition of this part, or null if unknown + * @throws MessagingException for failures + */ + @Override + public String getDisposition() throws MessagingException { + return MimeBodyPart.getDisposition(this); + } + + /** + * Set the disposition in the "Content-Disposition" header field + * of this body part. If the disposition is null, any existing + * "Content-Disposition" header field is removed. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setDisposition(String disposition) throws MessagingException { + MimeBodyPart.setDisposition(this, disposition); + } + + /** + * Returns the content transfer encoding from the + * "Content-Transfer-Encoding" header + * field. Returns null if the header is unavailable + * or its value is absent.

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return content-transfer-encoding + * @throws MessagingException for failures + */ + @Override + public String getEncoding() throws MessagingException { + return MimeBodyPart.getEncoding(this); + } + + /** + * Returns the value of the "Content-ID" header field. Returns + * null if the field is unavailable or its value is + * absent.

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return content-ID + * @throws MessagingException for failures + */ + @Override + public String getContentID() throws MessagingException { + return getHeader("Content-Id", null); + } + + /** + * Set the "Content-ID" header field of this Message. + * If the cid parameter is null, any existing + * "Content-ID" is removed. + * + * @param cid the content ID + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + public void setContentID(String cid) throws MessagingException { + if (cid == null) + removeHeader("Content-ID"); + else + setHeader("Content-ID", cid); + } + + /** + * Return the value of the "Content-MD5" header field. Returns + * null if this field is unavailable or its value + * is absent.

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return content-MD5 + * @throws MessagingException for failures + */ + @Override + public String getContentMD5() throws MessagingException { + return getHeader("Content-MD5", null); + } + + /** + * Set the "Content-MD5" header field of this Message. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setContentMD5(String md5) throws MessagingException { + setHeader("Content-MD5", md5); + } + + /** + * Returns the "Content-Description" header field of this Message. + * This typically associates some descriptive information with + * this part. Returns null if this field is unavailable or its + * value is absent.

+ *

+ * If the Content-Description field is encoded as per RFC 2047, + * it is decoded and converted into Unicode. If the decoding or + * conversion fails, the raw data is returned as-is

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return content-description + * @throws MessagingException for failures + */ + @Override + public String getDescription() throws MessagingException { + return MimeBodyPart.getDescription(this); + } + + /** + * Set the "Content-Description" header field for this Message. + * If the description parameter is null, then any + * existing "Content-Description" fields are removed.

+ *

+ * If the description contains non US-ASCII characters, it will + * be encoded using the platform's default charset. If the + * description contains only US-ASCII characters, no encoding + * is done and it is used as-is.

+ *

+ * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description content-description + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException An + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + */ + @Override + public void setDescription(String description) throws MessagingException { + setDescription(description, null); + } + + /** + * Set the "Content-Description" header field for this Message. + * If the description parameter is null, then any + * existing "Content-Description" fields are removed.

+ *

+ * If the description contains non US-ASCII characters, it will + * be encoded using the specified charset. If the description + * contains only US-ASCII characters, no encoding is done and + * it is used as-is.

+ *

+ * Note that if the charset encoding process fails, a + * MessagingException is thrown, and an UnsupportedEncodingException + * is included in the chain of nested exceptions within the + * MessagingException. + * + * @param description Description + * @param charset Charset for encoding + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException An + * UnsupportedEncodingException may be included + * in the exception chain if the charset + * conversion fails. + */ + public void setDescription(String description, String charset) + throws MessagingException { + MimeBodyPart.setDescription(this, description, charset); + } + + /** + * Get the languages specified in the "Content-Language" header + * field of this message. The Content-Language header is defined by + * RFC 1766. Returns null if this field is unavailable + * or its value is absent.

+ *

+ * This implementation uses the getHeader method + * to obtain the requisite header field. + * + * @return value of content-language header. + * @throws MessagingException for failures + */ + @Override + public String[] getContentLanguage() throws MessagingException { + return MimeBodyPart.getContentLanguage(this); + } + + /** + * Set the "Content-Language" header of this MimePart. The + * Content-Language header is defined by RFC 1766. + * + * @param languages array of language tags + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setContentLanguage(String[] languages) + throws MessagingException { + MimeBodyPart.setContentLanguage(this, languages); + } + + /** + * Returns the value of the "Message-ID" header field. Returns + * null if this field is unavailable or its value is absent.

+ *

+ * The default implementation provided here uses the + * getHeader method to return the value of the + * "Message-ID" field. + * + * @return Message-ID + * @throws MessagingException if the retrieval of this field + * causes any exception. + * @see jakarta.mail.search.MessageIDTerm + * @since JavaMail 1.1 + */ + public String getMessageID() throws MessagingException { + return getHeader("Message-ID", null); + } + + /** + * Get the filename associated with this Message.

+ *

+ * Returns the value of the "filename" parameter from the + * "Content-Disposition" header field of this message. If it's + * not available, returns the value of the "name" parameter from + * the "Content-Type" header field of this BodyPart. + * Returns null if both are absent.

+ *

+ * If the mail.mime.encodefilename System property + * is set to true, the {@link MimeUtility#decodeText + * MimeUtility.decodeText} method will be used to decode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @return filename + * @throws MessagingException for failures + */ + @Override + public String getFileName() throws MessagingException { + return MimeBodyPart.getFileName(this); + } + + /** + * Set the filename associated with this part, if possible.

+ *

+ * Sets the "filename" parameter of the "Content-Disposition" + * header field of this message.

+ *

+ * If the mail.mime.encodefilename System property + * is set to true, the {@link MimeUtility#encodeText + * MimeUtility.encodeText} method will be used to encode the + * filename. While such encoding is not supported by the MIME + * spec, many mailers use this technique to support non-ASCII + * characters in filenames. The default value of this property + * is false. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setFileName(String filename) throws MessagingException { + MimeBodyPart.setFileName(this, filename); + } + + private String getHeaderName(Message.RecipientType type) + throws MessagingException { + String headerName; + + if (type == Message.RecipientType.TO) + headerName = "To"; + else if (type == Message.RecipientType.CC) + headerName = "Cc"; + else if (type == Message.RecipientType.BCC) + headerName = "Bcc"; + else if (type == RecipientType.NEWSGROUPS) + headerName = "Newsgroups"; + else + throw new MessagingException("Invalid Recipient Type"); + return headerName; + } + + + /** + * Return a decoded input stream for this Message's "content".

+ *

+ * This implementation obtains the input stream from the DataHandler, + * that is, it invokes getDataHandler().getInputStream(). + * + * @return an InputStream + * @throws IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * jakarta.activation.DataHandler for more details. + * @throws MessagingException for other failures + * @see #getContentStream + * @see jakarta.activation.DataHandler#getInputStream + */ + @Override + public InputStream getInputStream() + throws IOException, MessagingException { + return getDataHandler().getInputStream(); + } + + /** + * Produce the raw bytes of the content. This method is used during + * parsing, to create a DataHandler object for the content. Subclasses + * that can provide a separate input stream for just the message + * content might want to override this method.

+ *

+ * This implementation returns a SharedInputStream, if + * contentStream is not null. Otherwise, it + * returns a ByteArrayInputStream constructed + * out of the content byte array. + * + * @return an InputStream containing the raw bytes + * @throws MessagingException for failures + * @see #content + */ + protected InputStream getContentStream() throws MessagingException { + if (contentStream != null) + return ((SharedInputStream) contentStream).newStream(0, -1); + if (content != null) { + return provider().inputSharedByteArray(content); + } + throw new MessagingException("No MimeMessage content"); + } + + /** + * Return an InputStream to the raw data with any Content-Transfer-Encoding + * intact. This method is useful if the "Content-Transfer-Encoding" + * header is incorrect or corrupt, which would prevent the + * getInputStream method or getContent method + * from returning the correct data. In such a case the application may + * use this method and attempt to decode the raw data itself.

+ *

+ * This implementation simply calls the getContentStream + * method. + * + * @return an InputStream containing the raw bytes + * @throws MessagingException for failures + * @see #getInputStream + * @see #getContentStream + * @since JavaMail 1.2 + */ + public InputStream getRawInputStream() throws MessagingException { + return getContentStream(); + } + + /** + * Return a DataHandler for this Message's content.

+ *

+ * The implementation provided here works approximately as follows. + * Note the use of the getContentStream method to + * generate the byte stream for the content. Also note that + * any transfer-decoding is done automatically within this method. + * + *

+     *  getDataHandler() {
+     *      if (dh == null) {
+     *          dh = new DataHandler(new MimePartDataSource(this));
+     *      }
+     *      return dh;
+     *  }
+     *
+     *  class MimePartDataSource implements DataSource {
+     *      public getInputStream() {
+     *          return MimeUtility.decode(
+     * 		     getContentStream(), getEncoding());
+     *      }
+     *
+     * 		.... <other DataSource methods>
+     *  }
+     * 
+ * + * @throws MessagingException for failures + */ + @Override + public synchronized DataHandler getDataHandler() + throws MessagingException { + if (dh == null) + dh = new MimeBodyPart.MimePartDataHandler(this); + return dh; + } + + /** + * This method provides the mechanism to set this part's content. + * The given DataHandler object should wrap the actual content. + * + * @param dh The DataHandler for the content. + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public synchronized void setDataHandler(DataHandler dh) + throws MessagingException { + this.dh = dh; + cachedContent = null; + MimeBodyPart.invalidateContentHeaders(this); + } + + /** + * Return the content as a Java object. The type of this + * object is dependent on the content itself. For + * example, the native format of a "text/plain" content + * is usually a String object. The native format for a "multipart" + * message is always a Multipart subclass. For content types that are + * unknown to the DataHandler system, an input stream is returned + * as the content.

+ *

+ * This implementation obtains the content from the DataHandler, + * that is, it invokes getDataHandler().getContent(). + * If the content is a Multipart or Message object and was created by + * parsing a stream, the object is cached and returned in subsequent + * calls so that modifications to the content will not be lost. + * + * @return Object + * @throws IOException this is typically thrown by the + * DataHandler. Refer to the documentation for + * jakarta.activation.DataHandler for more details. + * @throws MessagingException for other failures + * @see jakarta.mail.Part + * @see jakarta.activation.DataHandler#getContent + */ + @Override + public Object getContent() throws IOException, MessagingException { + if (cachedContent != null) + return cachedContent; + Object c; + try { + c = getDataHandler().getContent(); + } catch (IOException e) { + if (e.getCause() instanceof FolderClosedException) { + FolderClosedException fce = (FolderClosedException) e.getCause(); + throw new FolderClosedException(fce.getFolder(), e.getMessage(), e); + } else if (e.getCause() instanceof MessagingException) { + throw new MessageRemovedException(e.getMessage(), e); + } else { + throw e; + } + } + if (MimeBodyPart.cacheMultipart && + (c instanceof Multipart || c instanceof Message) && + (content != null || contentStream != null)) { + cachedContent = c; + /* + * We may abandon the input stream so make sure + * the MimeMultipart has consumed the stream. + */ + if (c instanceof MimeMultipart) + ((MimeMultipart) c).parse(); + } + return c; + } + + /** + * This method sets the Message's content to a Multipart object. + * + * @param mp The multipart object that is the Message's content + * @throws IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setContent(Multipart mp) throws MessagingException { + setDataHandler(new DataHandler(mp, mp.getContentType())); + mp.setParent(this); + } + + /** + * A convenience method for setting this Message's content.

+ *

+ * The content is wrapped in a DataHandler object. Note that a + * DataContentHandler class for the specified type should be + * available to the JavaMail implementation for this to work right. + * i.e., to do setContent(foobar, "application/x-foobar"), + * a DataContentHandler for "application/x-foobar" should be installed. + * Refer to the Java Activation Framework for more information. + * + * @param o the content object + * @param type Mime type of the object + * @throws IllegalWriteException if the underlying + * implementation does not support modification of + * existing values + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void setContent(Object o, String type) + throws MessagingException { + if (o instanceof Multipart) + setContent((Multipart) o); + else + setDataHandler(new DataHandler(o, type)); + } + + /** + * Convenience method that sets the given String as this + * part's content, with a MIME type of "text/plain". If the + * string contains non US-ASCII characters. it will be encoded + * using the platform's default charset. The charset is also + * used to set the "charset" parameter.

+ *

+ * Note that there may be a performance penalty if + * text is large, since this method may have + * to scan all the characters to determine what charset to + * use.

+ *

+ * If the charset is already known, use the + * setText method that takes the charset parameter. + * + * @param text the text content to set + * @throws MessagingException if an error occurs + * @see #setText(String text, String charset) + */ + @Override + public void setText(String text) throws MessagingException { + setText(text, null); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a MIME type of "text/plain" and the specified + * charset. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @throws MessagingException if an error occurs + */ + @Override + public void setText(String text, String charset) + throws MessagingException { + MimeBodyPart.setText(this, text, charset, "plain"); + } + + /** + * Convenience method that sets the given String as this part's + * content, with a primary MIME type of "text" and the specified + * MIME subtype. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @param subtype the MIME subtype to use (e.g., "html") + * @throws MessagingException if an error occurs + * @since JavaMail 1.4 + */ + @Override + public void setText(String text, String charset, String subtype) + throws MessagingException { + MimeBodyPart.setText(this, text, charset, subtype); + } + + /** + * Get a new Message suitable for a reply to this message. + * The new Message will have its attributes and headers + * set up appropriately. Note that this new message object + * will be empty, i.e., it will not have a "content". + * These will have to be suitably filled in by the client.

+ *

+ * If replyToAll is set, the new Message will be addressed + * to all recipients of this message. Otherwise, the reply will be + * addressed to only the sender of this message (using the value + * of the getReplyTo method).

+ *

+ * The "Subject" field is filled in with the original subject + * prefixed with "Re:" (unless it already starts with "Re:"). + * The "In-Reply-To" header is set in the new message if this + * message has a "Message-Id" header. The ANSWERED + * flag is set in this message. + *

+ * The current implementation also sets the "References" header + * in the new message to include the contents of the "References" + * header (or, if missing, the "In-Reply-To" header) in this message, + * plus the contents of the "Message-Id" header of this message, + * as described in RFC 2822. + * + * @param replyToAll reply should be sent to all recipients + * of this message + * @return the reply Message + * @throws MessagingException for failures + */ + @Override + public Message reply(boolean replyToAll) throws MessagingException { + return reply(replyToAll, true); + } + + /** + * Get a new Message suitable for a reply to this message. + * The new Message will have its attributes and headers + * set up appropriately. Note that this new message object + * will be empty, i.e., it will not have a "content". + * These will have to be suitably filled in by the client.

+ *

+ * If replyToAll is set, the new Message will be addressed + * to all recipients of this message. Otherwise, the reply will be + * addressed to only the sender of this message (using the value + * of the getReplyTo method).

+ *

+ * If setAnswered is set, the + * {@link Flags.Flag#ANSWERED ANSWERED} flag is set + * in this message.

+ *

+ * The "Subject" field is filled in with the original subject + * prefixed with "Re:" (unless it already starts with "Re:"). + * The "In-Reply-To" header is set in the new message if this + * message has a "Message-Id" header. + *

+ * The current implementation also sets the "References" header + * in the new message to include the contents of the "References" + * header (or, if missing, the "In-Reply-To" header) in this message, + * plus the contents of the "Message-Id" header of this message, + * as described in RFC 2822. + * + * @param replyToAll reply should be sent to all recipients + * of this message + * @param setAnswered set the ANSWERED flag in this message? + * @return the reply Message + * @throws MessagingException for failures + * @since JavaMail 1.5 + */ + public Message reply(boolean replyToAll, boolean setAnswered) + throws MessagingException { + MimeMessage reply = createMimeMessage(session); + /* + * Have to manipulate the raw Subject header so that we don't lose + * any encoding information. This is safe because "Re:" isn't + * internationalized and (generally) isn't encoded. If the entire + * Subject header is encoded, prefixing it with "Re: " still leaves + * a valid and correct encoded header. + */ + String subject = getHeader("Subject", null); + if (subject != null) { + if (!subject.regionMatches(true, 0, "Re: ", 0, 4)) + subject = "Re: " + subject; + reply.setHeader("Subject", subject); + } + Address[] a = getReplyTo(); + reply.setRecipients(Message.RecipientType.TO, a); + if (replyToAll) { + List

v = new ArrayList<>(); + // add my own address to list + InternetAddress me = InternetAddress.getLocalAddress(session); + if (me != null) + v.add(me); + // add any alternate names I'm known by + String alternates = null; + if (session != null) + alternates = session.getProperty("mail.alternates"); + if (alternates != null) + eliminateDuplicates(v, + InternetAddress.parse(alternates, false)); + // should we Cc all other original recipients? + boolean replyallcc = false; + if (session != null) + replyallcc = MimeUtility.getBooleanProperty( + session.getProperties(), + "mail.replyallcc", false); + // add the recipients from the To field so far + eliminateDuplicates(v, a); + a = getRecipients(Message.RecipientType.TO); + a = eliminateDuplicates(v, a); + if (a != null && a.length > 0) { + if (replyallcc) + reply.addRecipients(Message.RecipientType.CC, a); + else + reply.addRecipients(Message.RecipientType.TO, a); + } + a = getRecipients(Message.RecipientType.CC); + a = eliminateDuplicates(v, a); + if (a != null && a.length > 0) + reply.addRecipients(Message.RecipientType.CC, a); + // don't eliminate duplicate newsgroups + a = getRecipients(RecipientType.NEWSGROUPS); + if (a != null && a.length > 0) + reply.setRecipients(RecipientType.NEWSGROUPS, a); + } + + String msgId = getHeader("Message-Id", null); + if (msgId != null) + reply.setHeader("In-Reply-To", msgId); + + /* + * Set the References header as described in RFC 2822: + * + * The "References:" field will contain the contents of the parent's + * "References:" field (if any) followed by the contents of the parent's + * "Message-ID:" field (if any). If the parent message does not contain + * a "References:" field but does have an "In-Reply-To:" field + * containing a single message identifier, then the "References:" field + * will contain the contents of the parent's "In-Reply-To:" field + * followed by the contents of the parent's "Message-ID:" field (if + * any). If the parent has none of the "References:", "In-Reply-To:", + * or "Message-ID:" fields, then the new message will have no + * "References:" field. + */ + String refs = getHeader("References", " "); + if (refs == null) { + // XXX - should only use if it contains a single message identifier + refs = getHeader("In-Reply-To", " "); + } + if (msgId != null) { + if (refs != null) + refs = MimeUtility.unfold(refs) + " " + msgId; + else + refs = msgId; + } + if (refs != null) + reply.setHeader("References", MimeUtility.fold(12, refs)); + + if (setAnswered) { + try { + setFlags(answeredFlag, true); + } catch (MessagingException mex) { + // ignore it + } + } + return reply; + } + + /** + * Check addrs for any duplicates that may already be in v. + * Return a new array without the duplicates. Add any new + * addresses to v. Note that the input array may be modified. + */ + private Address[] eliminateDuplicates(List
v, Address[] addrs) { + if (addrs == null) + return null; + int gone = 0; + for (int i = 0; i < addrs.length; i++) { + boolean found = false; + // search the list for this address + for (int j = 0; j < v.size(); j++) { + if (v.get(j).equals(addrs[i])) { + // found it; count it and remove it from the input array + found = true; + gone++; + addrs[i] = null; + break; + } + } + if (!found) + v.add(addrs[i]); // add new address to list + } + // if we found any duplicates, squish the array + if (gone != 0) { + Address[] a; + // new array should be same type as original array + // XXX - there must be a better way, perhaps reflection? + if (addrs instanceof InternetAddress[]) + a = new InternetAddress[addrs.length - gone]; + else + a = new Address[addrs.length - gone]; + for (int i = 0, j = 0; i < addrs.length; i++) + if (addrs[i] != null) + a[j++] = addrs[i]; + addrs = a; + } + return addrs; + } + + /** + * Output the message as an RFC 822 format stream.

+ *

+ * Note that, depending on how the messag was constructed, it may + * use a variety of line termination conventions. Generally the + * output should be sent through an appropriate FilterOutputStream + * that converts the line terminators to the desired form, either + * CRLF for MIME compatibility and for use in Internet protocols, + * or the local platform's line terminator for storage in a local + * text file.

+ *

+ * This implementation calls the writeTo(OutputStream, + * String[]) method with a null ignore list. + * + * @throws IOException if an error occurs writing to the stream + * or if an error is generated by the + * jakarta.activation layer. + * @throws MessagingException for other failures + * @see jakarta.activation.DataHandler#writeTo + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + writeTo(os, null); + } + + /** + * Output the message as an RFC 822 format stream, without + * specified headers. If the saved flag is not set, + * the saveChanges method is called. + * If the modified flag is not + * set and the content array is not null, the + * content array is written directly, after + * writing the appropriate message headers. + * + * @param os the stream to write to + * @param ignoreList the headers to not include in the output + * @throws IOException if an error occurs writing to the stream + * or if an error is generated by the + * jakarta.activation layer. + * @throws MessagingException for other failures + * @see jakarta.activation.DataHandler#writeTo + */ + public void writeTo(OutputStream os, String[] ignoreList) + throws IOException, MessagingException { + if (!saved) + saveChanges(); + + if (modified) { + MimeBodyPart.writeTo(this, os, ignoreList); + return; + } + + // Else, the content is untouched, so we can just output it + // First, write out the header + Enumeration hdrLines = getNonMatchingHeaderLines(ignoreList); + LineOutputStream los = provider().outputLineStream(os, allowutf8); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + + // Finally, the content. + if (content == null) { + // call getContentStream to give subclass a chance to + // provide the data on demand + InputStream is = null; + byte[] buf = new byte[8192]; + try { + is = getContentStream(); + // now copy the data to the output stream + int len; + while ((len = is.read(buf)) > 0) + os.write(buf, 0, len); + } finally { + if (is != null) + is.close(); + buf = null; + } + } else { + os.write(content); + } + os.flush(); + } + + /** + * 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.

+ *

+ * This implementation obtains the headers from the + * headers InternetHeaders object. + * + * @param name name of header + * @return array of headers + * @throws MessagingException for failures + * @see MimeUtility + */ + @Override + public String[] getHeader(String name) + throws MessagingException { + 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 separator between values + * @return the value fields for all headers with + * this name + * @throws MessagingException for failures + */ + @Override + public String getHeader(String name, String delimiter) + throws MessagingException { + return headers.getHeader(name, delimiter); + } + + /** + * Set the value for this header_name. Replaces all existing + * header values with this new value. Note that RFC 822 headers + * must contain only US-ASCII characters, so a header that + * contains non US-ASCII characters must have been encoded by the + * caller as per the rules of RFC 2047. + * + * @param name header name + * @param value header value + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + * @see MimeUtility + */ + @Override + public void setHeader(String name, String value) + throws MessagingException { + headers.setHeader(name, value); + } + + /** + * Add this value to the existing values for this header_name. + * Note that RFC 822 headers must contain only US-ASCII + * characters, so a header that contains non US-ASCII characters + * must have been encoded as per the rules of RFC 2047. + * + * @param name header name + * @param value header value + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + * @see MimeUtility + */ + @Override + public void addHeader(String name, String value) + throws MessagingException { + headers.addHeader(name, value); + } + + /** + * Remove all headers with this name. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void removeHeader(String name) + throws MessagingException { + headers.removeHeader(name); + } + + /** + * 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.

+ *

+ * This implementation obtains the headers from the + * headers InternetHeaders object. + * + * @return array of header objects + * @throws MessagingException for failures + * @see MimeUtility + */ + @Override + public Enumeration

getAllHeaders() throws MessagingException { + return headers.getAllHeaders(); + } + + /** + * Return matching headers from this Message as an Enumeration of + * Header objects. This implementation obtains the headers from + * the headers InternetHeaders object. + * + * @throws MessagingException for failures + */ + @Override + public Enumeration
getMatchingHeaders(String[] names) + throws MessagingException { + return headers.getMatchingHeaders(names); + } + + /** + * Return non-matching headers from this Message as an + * Enumeration of Header objects. This implementation + * obtains the header from the headers InternetHeaders object. + * + * @throws MessagingException for failures + */ + @Override + public Enumeration
getNonMatchingHeaders(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaders(names); + } + + /** + * Add a raw RFC 822 header-line. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void addHeaderLine(String line) throws MessagingException { + headers.addHeaderLine(line); + } + + /** + * Get all header lines as an Enumeration of Strings. A Header + * line is a raw RFC 822 header-line, containing both the "name" + * and "value" field. + * + * @throws MessagingException for failures + */ + @Override + public Enumeration getAllHeaderLines() throws MessagingException { + return headers.getAllHeaderLines(); + } + + /** + * Get matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header-line, containing both + * the "name" and "value" field. + * + * @throws MessagingException for failures + */ + @Override + public Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getMatchingHeaderLines(names); + } + + /** + * Get non-matching header lines as an Enumeration of Strings. + * A Header line is a raw RFC 822 header-line, containing both + * the "name" and "value" field. + * + * @throws MessagingException for failures + */ + @Override + public Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException { + return headers.getNonMatchingHeaderLines(names); + } + + /** + * Return a Flags object containing the flags for + * this message.

+ *

+ * Note that a clone of the internal Flags object is returned, so + * modifying the returned Flags object will not affect the flags + * of this message. + * + * @return Flags object containing the flags for this message + * @throws MessagingException for failures + * @see Flags + */ + @Override + public synchronized Flags getFlags() throws MessagingException { + return (Flags) flags.clone(); + } + + /** + * Check whether the flag specified in the flag + * argument is set in this message.

+ *

+ * This implementation checks this message's internal + * flags object. + * + * @param flag the flag + * @return value of the specified flag for this message + * @throws MessagingException for failures + * @see Flags.Flag#ANSWERED + * @see Flags.Flag#DELETED + * @see Flags.Flag#DRAFT + * @see Flags.Flag#FLAGGED + * @see Flags.Flag#RECENT + * @see Flags.Flag#SEEN + * @see Flags.Flag + */ + @Override + public synchronized boolean isSet(Flags.Flag flag) + throws MessagingException { + return (flags.contains(flag)); + } + + /** + * Set the flags for this message.

+ *

+ * This implementation modifies the flags field. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public synchronized void setFlags(Flags flag, boolean set) + throws MessagingException { + if (set) + flags.add(flag); + else + flags.remove(flag); + } + + /** + * Updates the appropriate header fields of this message to be + * consistent with the message's contents. If this message is + * contained in a Folder, any changes made to this message are + * committed to the containing folder.

+ *

+ * If any part of a message's headers or contents are changed, + * saveChanges must be called to ensure that those + * changes are permanent. Otherwise, any such modifications may or + * may not be saved, depending on the folder implementation.

+ *

+ * Messages obtained from folders opened READ_ONLY should not be + * modified and saveChanges should not be called on such messages.

+ *

+ * This method sets the modified flag to true, the + * save flag to true, and then calls the + * updateHeaders method. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + @Override + public void saveChanges() throws MessagingException { + modified = true; + saved = true; + updateHeaders(); + } + + /** + * Update the Message-ID header. This method is called + * by the updateHeaders and allows a subclass + * to override only the algorithm for choosing a Message-ID. + * + * @throws MessagingException for failures + * @since JavaMail 1.4 + */ + protected void updateMessageID() throws MessagingException { + setHeader("Message-ID", + "<" + UniqueValue.getUniqueMessageIDValue(session) + ">"); + + } + + /** + * Called by the saveChanges method to actually + * update the MIME headers. The implementation here sets the + * Content-Transfer-Encoding header (if needed + * and not already set), the Date header (if + * not already set), the MIME-Version header + * and the Message-ID header. Also, if the content + * of this message is a MimeMultipart, its + * updateHeaders method is called.

+ *

+ * If the {@link #cachedContent} field is not null (that is, + * it references a Multipart or Message object), then + * that object is used to set a new DataHandler, any + * stream data used to create this object is discarded, + * and the {@link #cachedContent} field is cleared. + * + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @throws MessagingException for other failures + */ + protected synchronized void updateHeaders() throws MessagingException { + MimeBodyPart.updateHeaders(this); + setHeader("MIME-Version", "1.0"); + if (getHeader("Date") == null) + setSentDate(new Date()); + updateMessageID(); + + if (cachedContent != null) { + dh = new DataHandler(cachedContent, getContentType()); + cachedContent = null; + content = null; + if (contentStream != null) { + try { + contentStream.close(); + } catch (IOException ioex) { + } // nothing to do + } + contentStream = null; + } + } + + /** + * Create and return an InternetHeaders object that loads the + * headers from the given InputStream. Subclasses can override + * this method to return a subclass of InternetHeaders, if + * necessary. This implementation simply constructs and returns + * an InternetHeaders object. + * + * @param is the InputStream to read the headers from + * @return an InternetHeaders object + * @throws MessagingException for failures + * @since JavaMail 1.2 + */ + protected InternetHeaders createInternetHeaders(InputStream is) + throws MessagingException { + return new InternetHeaders(is, allowutf8); + } + + /** + * Create and return a MimeMessage object. The reply method + * uses this method to create the MimeMessage object that it + * will return. Subclasses can override this method to return + * a subclass of MimeMessage. This implementation simply constructs + * and returns a MimeMessage object using the supplied Session. + * + * @param session the Session to use for the new message + * @return the new MimeMessage object + * @throws MessagingException for failures + * @since JavaMail 1.4 + */ + protected MimeMessage createMimeMessage(Session session) + throws MessagingException { + return new MimeMessage(session); + } + + private StreamProvider provider() throws MessagingException { + try { + try { + final Session s = this.session; + if (s != null) { + return s.getStreamProvider(); + } else { + return Session.getDefaultInstance(System.getProperties(), + null).getStreamProvider(); + } + } catch (ServiceConfigurationError sce) { + throw new IllegalStateException(sce); + } + } catch (RuntimeException re) { + throw new MessagingException("Unable to get " + + StreamProvider.class.getName(), re); + } + } + + /** + * This inner class extends the jakarta.mail.Message.RecipientType + * class to add additional RecipientTypes. The one additional + * RecipientType currently defined here is NEWSGROUPS. + * + * @see Message.RecipientType + */ + @SuppressWarnings("serial") + public static class RecipientType extends Message.RecipientType { + + /** + * The "Newsgroup" (Usenet news) recipients. + */ + public static final RecipientType NEWSGROUPS = + new RecipientType("Newsgroups"); + + /** + * Constructor for use by subclasses. + * + * @param type the recipient type + */ + protected RecipientType(String type) { + super(type); + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/MimeMultipart.java b/net-mail/src/main/java/jakarta/mail/internet/MimeMultipart.java new file mode 100644 index 0000000..2ea5f27 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/MimeMultipart.java @@ -0,0 +1,1023 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import jakarta.activation.DataSource; +import jakarta.mail.BodyPart; +import jakarta.mail.IllegalWriteException; +import jakarta.mail.MessageAware; +import jakarta.mail.MessageContext; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.MultipartDataSource; +import jakarta.mail.util.LineInputStream; +import jakarta.mail.util.LineOutputStream; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + + +/** + * The MimeMultipart class is an implementation of the abstract Multipart + * class that uses MIME conventions for the multipart data.

+ *

+ * A MimeMultipart is obtained from a MimePart whose primary type + * is "multipart" (by invoking the part's getContent() method) + * or it can be created by a client as part of creating a new MimeMessage.

+ *

+ * The default multipart subtype is "mixed". The other multipart + * subtypes, such as "alternative", "related", and so on, can be + * implemented as subclasses of MimeMultipart with additional methods + * to implement the additional semantics of that type of multipart + * content. The intent is that service providers, mail JavaBean writers + * and mail clients will write many such subclasses and their Command + * Beans, and will install them into the JavaBeans Activation + * Framework, so that any Jakarta Mail implementation and its clients can + * transparently find and use these classes. Thus, a MIME multipart + * handler is treated just like any other type handler, thereby + * decoupling the process of providing multipart handlers from the + * Jakarta Mail API. Lacking these additional MimeMultipart subclasses, + * all subtypes of MIME multipart data appear as MimeMultipart objects.

+ *

+ * An application can directly construct a MIME multipart object of any + * subtype by using the MimeMultipart(String subtype) + * constructor. For example, to create a "multipart/alternative" object, + * use new MimeMultipart("alternative").

+ *

+ * The mail.mime.multipart.ignoremissingendboundary + * property may be set to false to cause a + * MessagingException to be thrown if the multipart + * data does not end with the required end boundary line. If this + * property is set to true or not set, missing end + * boundaries are not considered an error and the final body part + * ends at the end of the data.

+ *

+ * The mail.mime.multipart.ignoremissingboundaryparameter + * System property may be set to false to cause a + * MessagingException to be thrown if the Content-Type + * of the MimeMultipart does not include a boundary parameter. + * If this property is set to true or not set, the multipart + * parsing code will look for a line that looks like a bounary line and + * use that as the boundary separating the parts.

+ *

+ * The mail.mime.multipart.ignoreexistingboundaryparameter + * System property may be set to true to cause any boundary + * to be ignored and instead search for a boundary line in the message + * as with mail.mime.multipart.ignoremissingboundaryparameter.

+ *

+ * Normally, when writing out a MimeMultipart that contains no body + * parts, or when trying to parse a multipart message with no body parts, + * a MessagingException is thrown. The MIME spec does not allow + * multipart content with no body parts. The + * mail.mime.multipart.allowempty System property may be set to + * true to override this behavior. + * When writing out such a MimeMultipart, a single empty part will be + * included. When reading such a multipart, a MimeMultipart will be created + * with no body parts. + * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + */ + +public class MimeMultipart extends Multipart { + + /** + * The DataSource supplying our InputStream. + */ + protected DataSource ds = null; + + /** + * Have we parsed the data from our InputStream yet? + * Defaults to true; set to false when our constructor is + * given a DataSource with an InputStream that we need to + * parse. + */ + protected boolean parsed = true; + + /** + * Have we seen the final bounary line? + * + * @since JavaMail 1.5 + */ + protected boolean complete = true; + + /** + * The MIME multipart preamble text, the text that + * occurs before the first boundary line. + * + * @since JavaMail 1.5 + */ + protected String preamble = null; + + /** + * Flag corresponding to the "mail.mime.multipart.ignoremissingendboundary" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreMissingEndBoundary = true; + + /** + * Flag corresponding to the + * "mail.mime.multipart.ignoremissingboundaryparameter" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreMissingBoundaryParameter = true; + + /** + * Flag corresponding to the + * "mail.mime.multipart.ignoreexistingboundaryparameter" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean ignoreExistingBoundaryParameter = false; + + /** + * Flag corresponding to the "mail.mime.multipart.allowempty" + * property, set in the {@link #initializeProperties} method called from + * constructors and the parse method. + * + * @since JavaMail 1.5 + */ + protected boolean allowEmpty = false; + + /** + * Default constructor. An empty MimeMultipart object + * is created. Its content type is set to "multipart/mixed". + * A unique boundary string is generated and this string is + * setup as the "boundary" parameter for the + * contentType field.

+ *

+ * MimeBodyParts may be added later. + */ + public MimeMultipart() { + this("mixed"); + } + + /** + * Construct a MimeMultipart object of the given subtype. + * A unique boundary string is generated and this string is + * setup as the "boundary" parameter for the + * contentType field. + * Calls the {@link #initializeProperties} method.

+ *

+ * MimeBodyParts may be added later. + * + * @param subtype the MIME content subtype + */ + public MimeMultipart(String subtype) { + super(); + /* + * Compute a boundary string. + */ + String boundary = UniqueValue.getUniqueBoundaryValue(); + ContentType cType = new ContentType("multipart", subtype, null); + cType.setParameter("boundary", boundary); + contentType = cType.toString(); + initializeProperties(); + } + + /** + * Construct a MimeMultipart object of the default "mixed" subtype, + * and with the given body parts. More body parts may be added later. + * + * @param parts the body parts + * @throws MessagingException for failures + * @since JavaMail 1.5 + */ + @SuppressWarnings("this-escape") + public MimeMultipart(BodyPart... parts) throws MessagingException { + this(); + for (BodyPart bp : parts) { + super.addBodyPart(bp); + } + } + + /** + * Construct a MimeMultipart object of the given subtype + * and with the given body parts. More body parts may be added later. + * + * @param subtype the MIME content subtype + * @param parts the body parts + * @throws MessagingException for failures + * @since JavaMail 1.5 + */ + @SuppressWarnings("this-escape") + public MimeMultipart(String subtype, BodyPart... parts) + throws MessagingException { + this(subtype); + for (BodyPart bp : parts) { + super.addBodyPart(bp); + } + } + + /** + * Constructs a MimeMultipart object and its bodyparts from the + * given DataSource.

+ *

+ * This constructor handles as a special case the situation where the + * given DataSource is a MultipartDataSource object. In this case, this + * method just invokes the superclass (i.e., Multipart) constructor + * that takes a MultipartDataSource object.

+ *

+ * Otherwise, the DataSource is assumed to provide a MIME multipart + * byte stream. The parsed flag is set to false. When + * the data for the body parts are needed, the parser extracts the + * "boundary" parameter from the content type of this DataSource, + * skips the 'preamble' and reads bytes till the terminating + * boundary and creates MimeBodyParts for each part of the stream. + * + * @param ds DataSource, can be a MultipartDataSource + * @throws ParseException for failures parsing the message + * @throws MessagingException for other failures + */ + @SuppressWarnings("this-escape") + public MimeMultipart(DataSource ds) throws MessagingException { + super(); + + if (ds instanceof MessageAware) { + MessageContext mc = ((MessageAware) ds).getMessageContext(); + setParent(mc.getPart()); + } + + if (ds instanceof MultipartDataSource) { + // ask super to do this for us. + setMultipartDataSource((MultipartDataSource) ds); + return; + } + + // 'ds' was not a MultipartDataSource, we have + // to parse this ourself. + parsed = false; + this.ds = ds; + contentType = ds.getContentType(); + } + + /** + * Is the string all dashes ('-')? + */ + private static boolean allDashes(String s) { + for (int i = 0; i < s.length(); i++) { + if (s.charAt(i) != '-') + return false; + } + return true; + } + + /** + * Read data from the input stream to fill the buffer starting + * at the specified offset with the specified number of bytes. + * If len is zero, return zero. If at EOF, return -1. Otherwise, + * return the number of bytes read. Call the read method on the + * input stream as many times as necessary to read len bytes. + * + * @param in InputStream to read from + * @param buf buffer to read into + * @param off offset in the buffer for first byte + * @param len number of bytes to read + * @return -1 on EOF, otherwise number of bytes read + * @throws IOException on I/O errors + */ + private static int readFully(InputStream in, byte[] buf, int off, int len) + throws IOException { + if (len == 0) + return 0; + int total = 0; + while (len > 0) { + int bsize = in.read(buf, off, len); + if (bsize <= 0) // should never be zero + break; + off += bsize; + total += bsize; + len -= bsize; + } + return total > 0 ? total : -1; + } + + /** + * Initialize flags that control parsing behavior, + * based on System properties described above in + * the class documentation. + * + * @since JavaMail 1.5 + */ + private void initializeProperties() { + // read properties that control parsing + + // default to true + ignoreMissingEndBoundary = MimeUtility.getBooleanSystemProperty( + "mail.mime.multipart.ignoremissingendboundary", true); + // default to true + ignoreMissingBoundaryParameter = MimeUtility.getBooleanSystemProperty( + "mail.mime.multipart.ignoremissingboundaryparameter", true); + // default to false + ignoreExistingBoundaryParameter = MimeUtility.getBooleanSystemProperty( + "mail.mime.multipart.ignoreexistingboundaryparameter", false); + // default to false + allowEmpty = MimeUtility.getBooleanSystemProperty( + "mail.mime.multipart.allowempty", false); + } + + /** + * Set the subtype. This method should be invoked only on a new + * MimeMultipart object created by the client. The default subtype + * of such a multipart object is "mixed". + * + * @param subtype Subtype + * @throws MessagingException for failures + */ + public synchronized void setSubType(String subtype) + throws MessagingException { + ContentType cType = new ContentType(contentType); + cType.setSubType(subtype); + contentType = cType.toString(); + } + + /** + * Return the number of enclosed BodyPart objects. + * + * @return number of parts + */ + @Override + public synchronized int getCount() throws MessagingException { + parse(); + return super.getCount(); + } + + /** + * Get the specified BodyPart. BodyParts are numbered starting at 0. + * + * @param index the index of the desired BodyPart + * @return the Part + * @throws MessagingException if no such BodyPart exists + */ + @Override + public synchronized BodyPart getBodyPart(int index) + throws MessagingException { + parse(); + return super.getBodyPart(index); + } + + /** + * Get the MimeBodyPart referred to by the given ContentID (CID). + * Returns null if the part is not found. + * + * @param CID the ContentID of the desired part + * @return the Part + * @throws MessagingException for failures + */ + public synchronized BodyPart getBodyPart(String CID) + throws MessagingException { + parse(); + + int count = getCount(); + for (int i = 0; i < count; i++) { + MimeBodyPart part = (MimeBodyPart) getBodyPart(i); + String s = part.getContentID(); + if (s != null && s.equals(CID)) + return part; + } + return null; + } + + /** + * Remove the specified part from the multipart message. + * Shifts all the parts after the removed part down one. + * + * @param part The part to remove + * @return true if part removed, false otherwise + * @throws MessagingException if no such Part exists + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + */ + @Override + public boolean removeBodyPart(BodyPart part) throws MessagingException { + parse(); + return super.removeBodyPart(part); + } + + /** + * Remove the part at specified location (starting from 0). + * Shifts all the parts after the removed part down one. + * + * @param index Index of the part to remove + * @throws IndexOutOfBoundsException if the given index + * is out of range. + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws MessagingException for other failures + */ + @Override + public void removeBodyPart(int index) throws MessagingException { + parse(); + super.removeBodyPart(index); + } + + /** + * Adds a Part to the multipart. The BodyPart is appended to + * the list of existing Parts. + * + * @param part The Part to be appended + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws MessagingException for other failures + */ + @Override + public synchronized void addBodyPart(BodyPart part) + throws MessagingException { + parse(); + super.addBodyPart(part); + } + + /** + * Adds a BodyPart at position index. + * If index is not the last one in the list, + * the subsequent parts are shifted up. If index + * is larger than the number of parts present, the + * BodyPart is appended to the end. + * + * @param part The BodyPart to be inserted + * @param index Location where to insert the part + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * of existing values + * @throws MessagingException for other failures + */ + @Override + public synchronized void addBodyPart(BodyPart part, int index) + throws MessagingException { + parse(); + super.addBodyPart(part, index); + } + + /** + * Return true if the final boundary line for this + * multipart was seen. When parsing multipart content, + * this class will (by default) terminate parsing with + * no error if the end of input is reached before seeing + * the final multipart boundary line. In such a case, + * this method will return false. (If the System property + * "mail.mime.multipart.ignoremissingendboundary" is set to + * false, parsing such a message will instead throw a + * MessagingException.) + * + * @return true if the final boundary line was seen + * @throws MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized boolean isComplete() throws MessagingException { + parse(); + return complete; + } + + /** + * Get the preamble text, if any, that appears before the + * first body part of this multipart. Some protocols, + * such as IMAP, will not allow access to the preamble text. + * + * @return the preamble text, or null if no preamble + * @throws MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized String getPreamble() throws MessagingException { + parse(); + return preamble; + } + + /** + * Set the preamble text to be included before the first + * body part. Applications should generally not include + * any preamble text. In some cases it may be helpful to + * include preamble text with instructions for users of + * pre-MIME software. The preamble text should be complete + * lines, including newlines. + * + * @param preamble the preamble text + * @throws MessagingException for failures + * @since JavaMail 1.4 + */ + public synchronized void setPreamble(String preamble) + throws MessagingException { + this.preamble = preamble; + } + + /** + * Update headers. The default implementation here just + * calls the updateHeaders method on each of its + * children BodyParts.

+ *

+ * Note that the boundary parameter is already set up when + * a new and empty MimeMultipart object is created.

+ *

+ * This method is called when the saveChanges + * method is invoked on the Message object containing this + * Multipart. This is typically done as part of the Message + * send process, however note that a client is free to call + * it any number of times. So if the header updating process is + * expensive for a specific MimeMultipart subclass, then it + * might itself want to track whether its internal state actually + * did change, and do the header updating only if necessary. + * + * @throws MessagingException for failures + */ + protected synchronized void updateHeaders() throws MessagingException { + parse(); + for (int i = 0; i < parts.size(); i++) + ((MimeBodyPart) parts.elementAt(i)).updateHeaders(); + } + + /** + * Iterates through all the parts and outputs each MIME part + * separated by a boundary. + */ + @Override + public synchronized void writeTo(OutputStream os) + throws IOException, MessagingException { + parse(); + + String boundary = "--" + + (new ContentType(contentType)).getParameter("boundary"); + LineOutputStream los = streamProvider.outputLineStream(os, false); + // if there's a preamble, write it out + if (preamble != null) { + byte[] pb = MimeUtility.getBytes(preamble); + los.write(pb); + // make sure it ends with a newline + if (pb.length > 0 && + !(pb[pb.length - 1] == '\r' || pb[pb.length - 1] == '\n')) { + los.writeln(); + } + // XXX - could force a blank line before start boundary + } + + if (parts.size() == 0) { + if (allowEmpty) { + // write out a single empty body part + los.writeln(boundary); // put out boundary + los.writeln(); // put out empty line + } else { + throw new MessagingException("Empty multipart: " + contentType); + } + } else { + for (int i = 0; i < parts.size(); i++) { + los.writeln(boundary); // put out boundary + parts.elementAt(i).writeTo(os); + los.writeln(); // put out empty line + } + } + + // put out last boundary + los.writeln(boundary + "--"); + } + + /** + * Parse the InputStream from our DataSource, constructing the + * appropriate MimeBodyParts. The parsed flag is + * set to true, and if true on entry nothing is done. This + * method is called by all other methods that need data for + * the body parts, to make sure the data has been parsed. + * The {@link #initializeProperties} method is called before + * parsing the data. + * + * @throws ParseException for failures parsing the message + * @throws MessagingException for other failures + * @since JavaMail 1.2 + */ + protected synchronized void parse() throws MessagingException { + if (parsed) + return; + + initializeProperties(); + + InputStream in = null; + SharedInputStream sin = null; + long start = 0, end = 0; + + try { + in = ds.getInputStream(); + if (!(in instanceof ByteArrayInputStream) && + !(in instanceof BufferedInputStream) && + !(in instanceof SharedInputStream)) + in = new BufferedInputStream(in); + } catch (Exception ex) { + throw new MessagingException("No inputstream from datasource", ex); + } + if (in instanceof SharedInputStream) + sin = (SharedInputStream) in; + + ContentType cType = new ContentType(contentType); + String boundary = null; + if (!ignoreExistingBoundaryParameter) { + String bp = cType.getParameter("boundary"); + if (bp != null) + boundary = "--" + bp; + } + if (boundary == null && !ignoreMissingBoundaryParameter && + !ignoreExistingBoundaryParameter) + throw new ParseException("Missing boundary parameter"); + + try { + // Skip and save the preamble + LineInputStream lin = streamProvider.inputLineStream(in, false); + StringBuilder preamblesb = null; + String line; + while ((line = lin.readLine()) != null) { + /* + * Strip trailing whitespace. Can't use trim method + * because it's too aggressive. Some bogus MIME + * messages will include control characters in the + * boundary string. + */ + int i; + for (i = line.length() - 1; i >= 0; i--) { + char c = line.charAt(i); + if (!(c == ' ' || c == '\t')) + break; + } + line = line.substring(0, i + 1); + if (boundary != null) { + if (line.equals(boundary)) + break; + if (line.length() == boundary.length() + 2 && + line.startsWith(boundary) && line.endsWith("--")) { + line = null; // signal end of multipart + break; + } + } else { + /* + * Boundary hasn't been defined, does this line + * look like a boundary? If so, assume it is + * the boundary and save it. + */ + if (line.length() > 2 && line.startsWith("--")) { + if (line.length() > 4 && allDashes(line)) { + /* + * The first boundary-like line we find is + * probably *not* the end-of-multipart boundary + * line. More likely it's a line full of dashes + * in the preamble text. Just keep reading. + */ + } else { + boundary = line; + break; + } + } + } + + // save the preamble after skipping blank lines + if (line.length() > 0) { + // accumulate the preamble + if (preamblesb == null) + preamblesb = new StringBuilder(line.length() + 2); + preamblesb.append(line).append(System.lineSeparator()); + } + } + + if (preamblesb != null) + preamble = preamblesb.toString(); + + if (line == null) { + if (allowEmpty) + return; + else + throw new ParseException("Missing start boundary"); + } + + // save individual boundary bytes for comparison later + byte[] bndbytes = MimeUtility.getBytes(boundary); + int bl = bndbytes.length; + + /* + * Compile Boyer-Moore parsing tables. + */ + + // initialize Bad Character Shift table + int[] bcs = new int[256]; + for (int i = 0; i < bl; i++) + bcs[bndbytes[i] & 0xff] = i + 1; + + // initialize Good Suffix Shift table + int[] gss = new int[bl]; + NEXT: + for (int i = bl; i > 0; i--) { + int j; // the beginning index of the suffix being considered + for (j = bl - 1; j >= i; j--) { + // Testing for good suffix + if (bndbytes[j] == bndbytes[j - i]) { + // bndbytes[j..len] is a good suffix + gss[j - 1] = i; + } else { + // No match. The array has already been + // filled up with correct values before. + continue NEXT; + } + } + while (j > 0) + gss[--j] = i; + } + gss[bl - 1] = 1; + + /* + * Read and process body parts until we see the + * terminating boundary line (or EOF). + */ + boolean done = false; + getparts: + while (!done) { + InternetHeaders headers = null; + if (sin != null) { + start = sin.getPosition(); + // skip headers + while ((line = lin.readLine()) != null && line.length() > 0) + ; + if (line == null) { + if (!ignoreMissingEndBoundary) + throw new ParseException( + "missing multipart end boundary"); + // assume there's just a missing end boundary + complete = false; + break getparts; + } + } else { + // collect the headers for this body part + headers = createInternetHeaders(in); + } + + if (!in.markSupported()) + throw new MessagingException("Stream doesn't support mark"); + + ByteArrayOutputStream buf = null; + // if we don't have a shared input stream, we copy the data + if (sin == null) + buf = new ByteArrayOutputStream(); + else + end = sin.getPosition(); + int b; + + /* + * These buffers contain the bytes we're checking + * for a match. inbuf is the current buffer and + * previnbuf is the previous buffer. We need the + * previous buffer to check that we're preceeded + * by an EOL. + */ + // XXX - a smarter algorithm would use a sliding window + // over a larger buffer + byte[] inbuf = new byte[bl]; + byte[] previnbuf = new byte[bl]; + int inSize = 0; // number of valid bytes in inbuf + int prevSize = 0; // number of valid bytes in previnbuf + int eolLen; + boolean first = true; + + /* + * Read and save the content bytes in buf. + */ + for (; ; ) { + in.mark(bl + 4 + 1000); // bnd + "--\r\n" + lots of LWSP + eolLen = 0; + inSize = readFully(in, inbuf, 0, bl); + if (inSize < bl) { + // hit EOF + if (!ignoreMissingEndBoundary) + throw new ParseException( + "missing multipart end boundary"); + if (sin != null) + end = sin.getPosition(); + complete = false; + done = true; + break; + } + // check whether inbuf contains a boundary string + int i; + for (i = bl - 1; i >= 0; i--) { + if (inbuf[i] != bndbytes[i]) + break; + } + if (i < 0) { // matched all bytes + eolLen = 0; + if (!first) { + // working backwards, find out if we were preceeded + // by an EOL, and if so find its length + b = previnbuf[prevSize - 1]; + if (b == '\r' || b == '\n') { + eolLen = 1; + if (b == '\n' && prevSize >= 2) { + b = previnbuf[prevSize - 2]; + if (b == '\r') + eolLen = 2; + } + } + } + if (first || eolLen > 0) { // yes, preceed by EOL + if (sin != null) { + // update "end", in case this really is + // a valid boundary + end = sin.getPosition() - bl - eolLen; + } + // matched the boundary, check for last boundary + int b2 = in.read(); + if (b2 == '-') { + if (in.read() == '-') { + complete = true; + done = true; + break; // ignore trailing text + } + } + // skip linear whitespace + while (b2 == ' ' || b2 == '\t') + b2 = in.read(); + // check for end of line + if (b2 == '\n') + break; // got it! break out of the loop + if (b2 == '\r') { + in.mark(1); + if (in.read() != '\n') + in.reset(); + break; // got it! break out of the loop + } + } + i = 0; + } + + /* + * Get here if boundary didn't match, + * wasn't preceeded by EOL, or wasn't + * followed by whitespace or EOL. + */ + + // compute how many bytes we can skip + int skip = Math.max(i + 1 - bcs[inbuf[i] & 0xff], gss[i]); + // want to keep at least two characters + if (skip < 2) { + // only skipping one byte, save one byte + // from previous buffer as well + // first, write out bytes we're done with + if (sin == null && prevSize > 1) + buf.write(previnbuf, 0, prevSize - 1); + in.reset(); + skipFully(in, 1); + if (prevSize >= 1) { // is there a byte to save? + // yes, save one from previous and one from current + previnbuf[0] = previnbuf[prevSize - 1]; + previnbuf[1] = inbuf[0]; + prevSize = 2; + } else { + // no previous bytes to save, can only save current + previnbuf[0] = inbuf[0]; + prevSize = 1; + } + } else { + // first, write out data from previous buffer before + // we dump it + if (prevSize > 0 && sin == null) + buf.write(previnbuf, 0, prevSize); + // all the bytes we're skipping are saved in previnbuf + prevSize = skip; + in.reset(); + skipFully(in, prevSize); + // swap buffers + byte[] tmp = inbuf; + inbuf = previnbuf; + previnbuf = tmp; + } + first = false; + } + + /* + * Create a MimeBody element to represent this body part. + */ + MimeBodyPart part; + if (sin != null) { + part = createMimeBodyPartIs(sin.newStream(start, end)); + } else { + // write out data from previous buffer, not including EOL + if (prevSize - eolLen > 0) + buf.write(previnbuf, 0, prevSize - eolLen); + // if we didn't find a trailing boundary, + // the current buffer has data we need too + if (!complete && inSize > 0) + buf.write(inbuf, 0, inSize); + part = createMimeBodyPart(headers, buf.toByteArray()); + } + super.addBodyPart(part); + } + } catch (IOException ioex) { + throw new MessagingException("IO Error", ioex); + } finally { + try { + in.close(); + } catch (IOException cex) { + // ignore + } + } + + parsed = true; + } + + /** + * Skip the specified number of bytes, repeatedly calling + * the skip method as necessary. + */ + private void skipFully(InputStream in, long offset) throws IOException { + while (offset > 0) { + long cur = in.skip(offset); + if (cur <= 0) + throw new EOFException("can't skip"); + offset -= cur; + } + } + + /** + * Create and return an InternetHeaders object that loads the + * headers from the given InputStream. Subclasses can override + * this method to return a subclass of InternetHeaders, if + * necessary. This implementation simply constructs and returns + * an InternetHeaders object. + * + * @param is the InputStream to read the headers from + * @return an InternetHeaders object + * @throws MessagingException for failures + * @since JavaMail 1.2 + */ + protected InternetHeaders createInternetHeaders(InputStream is) + throws MessagingException { + return new InternetHeaders(is); + } + + /** + * Create and return a MimeBodyPart object to represent a + * body part parsed from the InputStream. Subclasses can override + * this method to return a subclass of MimeBodyPart, if + * necessary. This implementation simply constructs and returns + * a MimeBodyPart object. + * + * @param headers the headers for the body part + * @param content the content of the body part + * @return a MimeBodyPart + * @throws MessagingException for failures + * @since JavaMail 1.2 + */ + protected MimeBodyPart createMimeBodyPart(InternetHeaders headers, + byte[] content) throws MessagingException { + return new MimeBodyPart(headers, content); + } + + /** + * Create and return a MimeBodyPart object to represent a + * body part parsed from the InputStream. Subclasses can override + * this method to return a subclass of MimeBodyPart, if + * necessary. This implementation simply constructs and returns + * a MimeBodyPart object. + * + * @param is InputStream containing the body part + * @return a MimeBodyPart + * @throws MessagingException for failures + * @since JavaMail 1.2 + */ + protected MimeBodyPart createMimeBodyPart(InputStream is) + throws MessagingException { + return new MimeBodyPart(is); + } + + private MimeBodyPart createMimeBodyPartIs(InputStream is) + throws MessagingException { + try { + return createMimeBodyPart(is); + } finally { + try { + is.close(); + } catch (IOException ex) { + // ignore it + } + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/MimePart.java b/net-mail/src/main/java/jakarta/mail/internet/MimePart.java new file mode 100644 index 0000000..bd3cd9c --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/MimePart.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import jakarta.mail.IllegalWriteException; +import jakarta.mail.MessagingException; +import jakarta.mail.Part; +import java.util.Enumeration; + +/** + * The MimePart interface models an Entity as defined + * by MIME (RFC2045, Section 2.4).

+ *

+ * MimePart extends the Part interface to add additional RFC822 and MIME + * specific semantics and attributes. It provides the base interface for + * the MimeMessage and MimeBodyPart classes + * + *


A note on RFC822 and MIME headers

+ *

+ * RFC822 and MIME header fields must contain only + * US-ASCII characters. If a header contains non US-ASCII characters, + * it must be encoded as per the rules in RFC 2047. The MimeUtility + * class provided in this package can be used to to achieve this. + * Callers of the setHeader, addHeader, and + * addHeaderLine methods are responsible for enforcing + * the MIME requirements for the specified headers. In addition, these + * header fields must be folded (wrapped) before being sent if they + * exceed the line length limitation for the transport (1000 bytes for + * SMTP). Received headers may have been folded. The application is + * responsible for folding and unfolding headers as appropriate. + * + * @author John Mani + * @see MimeUtility + * @see Part + */ + +public interface MimePart extends Part { + + /** + * Get the values of all header fields available for this header, + * returned as a single String, with the values separated by the + * delimiter. If the delimiter is null, only the + * first value is returned. + * + * @param name the name of this header + * @param delimiter delimiter between fields in returned string + * @return the value fields for all headers with + * this name + * @throws MessagingException for failures + */ + String getHeader(String name, String delimiter) + throws MessagingException; + + /** + * Add a raw RFC822 header-line. + * + * @param line the line to add + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this Part is + * obtained from a READ_ONLY folder + * @throws MessagingException for other failures + */ + void addHeaderLine(String line) throws MessagingException; + + /** + * 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. + * + * @return an Enumeration of Strings + * @throws MessagingException for failures + */ + Enumeration getAllHeaderLines() throws MessagingException; + + /** + * 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. + * + * @param names the headers to return + * @return an Enumeration of Strings + * @throws MessagingException for failures + */ + Enumeration getMatchingHeaderLines(String[] names) + throws MessagingException; + + /** + * 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. + * + * @param names the headers to not return + * @return an Enumeration of Strings + * @throws MessagingException for failures + */ + Enumeration getNonMatchingHeaderLines(String[] names) + throws MessagingException; + + /** + * Get the transfer encoding of this part. + * + * @return content-transfer-encoding + * @throws MessagingException for failures + */ + String getEncoding() throws MessagingException; + + /** + * Get the Content-ID of this part. Returns null if none present. + * + * @return content-ID + * @throws MessagingException for failures + */ + String getContentID() throws MessagingException; + + /** + * Get the Content-MD5 digest of this part. Returns null if + * none present. + * + * @return content-MD5 + * @throws MessagingException for failures + */ + String getContentMD5() throws MessagingException; + + /** + * Set the Content-MD5 of this part. + * + * @param md5 the MD5 value + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this Part is + * obtained from a READ_ONLY folder + */ + void setContentMD5(String md5) throws MessagingException; + + /** + * Get the language tags specified in the Content-Language header + * of this MimePart. The Content-Language header is defined by + * RFC 1766. Returns null if this header is not + * available. + * + * @return array of content language strings + * @throws MessagingException for failures + */ + String[] getContentLanguage() throws MessagingException; + + /** + * Set the Content-Language header of this MimePart. The + * Content-Language header is defined by RFC1766. + * + * @param languages array of language tags + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @throws IllegalStateException if this Part is + * obtained from a READ_ONLY folder + */ + void setContentLanguage(String[] languages) + throws MessagingException; + + /** + * Convenience method that sets the given String as this + * part's content, with a MIME type of "text/plain". If the + * string contains non US-ASCII characters. it will be encoded + * using the platform's default charset. The charset is also + * used to set the "charset" parameter.

+ *

+ * Note that there may be a performance penalty if + * text is large, since this method may have + * to scan all the characters to determine what charset to + * use.

+ *

+ * If the charset is already known, use the + * setText method that takes the charset parameter. + * + * @param text the text content to set + * @throws MessagingException if an error occurs + * @see #setText(String text, String charset) + */ + @Override + void setText(String text) throws MessagingException; + + /** + * Convenience method that sets the given String as this part's + * content, with a MIME type of "text/plain" and the specified + * charset. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @throws MessagingException if an error occurs + */ + void setText(String text, String charset) + throws MessagingException; + + /** + * Convenience method that sets the given String as this part's + * content, with a primary MIME type of "text" and the specified + * MIME subtype. The given Unicode string will be charset-encoded + * using the specified charset. The charset is also used to set + * the "charset" parameter. + * + * @param text the text content to set + * @param charset the charset to use for the text + * @param subtype the MIME subtype to use (e.g., "html") + * @throws MessagingException if an error occurs + * @since JavaMail 1.4 + */ + void setText(String text, String charset, String subtype) + throws MessagingException; +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/MimePartDataSource.java b/net-mail/src/main/java/jakarta/mail/internet/MimePartDataSource.java new file mode 100644 index 0000000..7380de8 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/MimePartDataSource.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import jakarta.activation.DataSource; +import jakarta.mail.FolderClosedException; +import jakarta.mail.MessageAware; +import jakarta.mail.MessageContext; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.UnknownServiceException; + + +/** + * A utility class that implements a DataSource out of + * a MimePart. This class is primarily meant for service providers. + * + * @author John Mani + * @see MimePart + * @see jakarta.activation.DataSource + */ + +public class MimePartDataSource implements DataSource, MessageAware { + /** + * The MimePart that provides the data for this DataSource. + * + * @since JavaMail 1.4 + */ + protected MimePart part; + + private MessageContext context; + + /** + * Constructor, that constructs a DataSource from a MimePart. + * + * @param part the MimePart + */ + public MimePartDataSource(MimePart part) { + this.part = part; + } + + /** + * Returns an input stream from this MimePart.

+ *

+ * This method applies the appropriate transfer-decoding, based + * on the Content-Transfer-Encoding attribute of this MimePart. + * Thus the returned input stream is a decoded stream of bytes.

+ *

+ * This implementation obtains the raw content from the Part + * using the getContentStream() method and decodes + * it using the MimeUtility.decode() method. + * + * @return decoded input stream + * @see MimeMessage#getContentStream + * @see MimeBodyPart#getContentStream + * @see MimeUtility#decode + */ + @Override + public InputStream getInputStream() throws IOException { + InputStream is; + + try { + if (part instanceof MimeBodyPart) + is = ((MimeBodyPart) part).getContentStream(); + else if (part instanceof MimeMessage) + is = ((MimeMessage) part).getContentStream(); + else + throw new MessagingException("Unknown part"); + + String encoding = + MimeBodyPart.restrictEncoding(part, part.getEncoding()); + if (encoding != null) + return MimeUtility.decode(is, encoding); + else + return is; + } catch (FolderClosedException fex) { + throw new IOException(new FolderClosedException(fex.getFolder(), fex.getMessage(), fex)); + } catch (MessagingException mex) { + // Cause is removed because in upper stack the cause is checked. See test POP3FolderClosedExceptionTest + throw new IOException(mex.getMessage()); + } + } + + /** + * DataSource method to return an output stream.

+ *

+ * This implementation throws the UnknownServiceException. + */ + @Override + public OutputStream getOutputStream() throws IOException { + throw new UnknownServiceException("Writing not supported"); + } + + /** + * Returns the content-type of this DataSource.

+ *

+ * This implementation just invokes the getContentType + * method on the MimePart. + */ + @Override + public String getContentType() { + try { + return part.getContentType(); + } catch (MessagingException mex) { + // would like to be able to reflect the exception to the + // application, but since we can't do that we return a + // generic "unknown" value here and hope for another + // exception later. + return "application/octet-stream"; + } + } + + /** + * DataSource method to return a name.

+ *

+ * This implementation just returns an empty string. + */ + @Override + public String getName() { + try { + if (part instanceof MimeBodyPart) + return part.getFileName(); + } catch (MessagingException mex) { + // ignore it + } + return ""; + } + + /** + * Return the MessageContext for the current part. + * + * @since JavaMail 1.1 + */ + @Override + public synchronized MessageContext getMessageContext() { + if (context == null) + context = new MessageContext(part); + return context; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/MimeUtil.java b/net-mail/src/main/java/jakarta/mail/internet/MimeUtil.java new file mode 100644 index 0000000..aa5c118 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/MimeUtil.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2010, 2023 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 jakarta.mail.internet; + +import java.lang.reflect.Method; + +/** + * General MIME-related utility methods. + * + * @author Bill Shannon + * @since JavaMail 1.4.4 + */ +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", + MimePart.class, String.class); + } + } catch (ClassNotFoundException | RuntimeException | NoSuchMethodException 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 Thread.currentThread().getContextClassLoader(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/MimeUtility.java b/net-mail/src/main/java/jakarta/mail/internet/MimeUtility.java new file mode 100644 index 0000000..68d35e8 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/MimeUtility.java @@ -0,0 +1,1810 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import jakarta.activation.DataHandler; +import jakarta.activation.DataSource; +import jakarta.mail.EncodingAware; +import jakarta.mail.MessagingException; +import jakarta.mail.util.LineInputStream; +import jakarta.mail.util.StreamProvider; +import jakarta.mail.util.StreamProvider.EncoderTypes; +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Properties; +import java.util.StringTokenizer; + +/** + * This is a utility class that provides various MIME related + * functionality.

+ *

+ * There are a set of methods to encode and decode MIME headers as + * per RFC 2047. Note that, in general, these methods are + * not needed when using methods such as + * setSubject and setRecipients; Jakarta Mail + * will automatically encode and decode data when using these "higher + * level" methods. The methods below are only needed when maniuplating + * raw MIME headers using setHeader and getHeader + * methods. A brief description on handling such headers is given below:

+ *

+ * RFC 822 mail headers must contain only US-ASCII + * characters. Headers that contain non US-ASCII characters must be + * encoded so that they contain only US-ASCII characters. Basically, + * this process involves using either BASE64 or QP to encode certain + * characters. RFC 2047 describes this in detail.

+ *

+ * In Java, Strings contain (16 bit) Unicode characters. ASCII is a + * subset of Unicode (and occupies the range 0 - 127). A String + * that contains only ASCII characters is already mail-safe. If the + * String contains non US-ASCII characters, it must be encoded. An + * additional complexity in this step is that since Unicode is not + * yet a widely used charset, one might want to first charset-encode + * the String into another charset and then do the transfer-encoding. + *

+ * Note that to get the actual bytes of a mail-safe String (say, + * for sending over SMTP), one must do + *

+ *
+ * 	byte[] bytes = string.getBytes("iso-8859-1");
+ *
+ * 

+ *

+ * The setHeader and addHeader methods + * on MimeMessage and MimeBodyPart assume that the given header values + * are Unicode strings that contain only US-ASCII characters. Hence + * the callers of those methods must insure that the values they pass + * do not contain non US-ASCII characters. The methods in this class + * help do this.

+ *

+ * The getHeader family of methods on MimeMessage and + * MimeBodyPart return the raw header value. These might be encoded + * as per RFC 2047, and if so, must be decoded into Unicode Strings. + * The methods in this class help to do this.

+ *

+ * Several System properties control strict conformance to the MIME + * spec. Note that these are not session properties but must be set + * globally as System properties.

+ *

+ * The mail.mime.decodetext.strict property controls + * decoding of MIME encoded words. The MIME spec requires that encoded + * words start at the beginning of a whitespace separated word. Some + * mailers incorrectly include encoded words in the middle of a word. + * If the mail.mime.decodetext.strict System property is + * set to "false", an attempt will be made to decode these + * illegal encoded words. The default is true.

+ *

+ * The mail.mime.encodeeol.strict property controls the + * choice of Content-Transfer-Encoding for MIME parts that are not of + * type "text". Often such parts will contain textual data for which + * an encoding that allows normal end of line conventions is appropriate. + * In rare cases, such a part will appear to contain entirely textual + * data, but will require an encoding that preserves CR and LF characters + * without change. If the mail.mime.encodeeol.strict + * System property is set to "true", such an encoding will + * be used when necessary. The default is false.

+ *

+ * In addition, the mail.mime.charset System property can + * be used to specify the default MIME charset to use for encoded words + * and text parts that don't otherwise specify a charset. Normally, the + * default MIME charset is derived from the default Java charset, as + * specified in the file.encoding System property. Most + * applications will have no need to explicitly set the default MIME + * charset. In cases where the default MIME charset to be used for + * mail messages is different than the charset used for files stored on + * the system, this property should be set.

+ *

+ * The current implementation also supports the following System property. + *

+ * The mail.mime.ignoreunknownencoding property controls + * whether unknown values in the Content-Transfer-Encoding + * header, as passed to the decode method, cause an exception. + * If set to "true", unknown values are ignored and 8bit + * encoding is assumed. Otherwise, unknown values cause a MessagingException + * to be thrown. + * + * @author John Mani + * @author Bill Shannon + */ + +public class MimeUtility { + + public static final int ALL = -1; + static final int ALL_ASCII = 1; + static final int MOSTLY_ASCII = 2; + static final int MOSTLY_NONASCII = 3; + // cached map of whether a charset is compatible with ASCII + // Map + private static final Map nonAsciiCharsetMap + = new HashMap<>(); + private static final String WORD_SPECIALS = "=_?\"#$%&'(),.:;<>@[\\]^`{|}~"; + private static final String TEXT_SPECIALS = "=_?"; + private static final boolean decodeStrict = getBooleanSystemProperty("mail.mime.decodetext.strict", true); + private static final boolean encodeEolStrict = getBooleanSystemProperty("mail.mime.encodeeol.strict", false); + private static final boolean ignoreUnknownEncoding = getBooleanSystemProperty( + "mail.mime.ignoreunknownencoding", false); + private static final boolean allowUtf8 = getBooleanSystemProperty("mail.mime.allowutf8", false); + /* + * The following two properties allow disabling the fold() + * and unfold() methods and reverting to the previous behavior. + * They should never need to be changed and are here only because + * of my paranoid concern with compatibility. + */ + private static final boolean foldEncodedWords = getBooleanSystemProperty("mail.mime.foldencodedwords", false); + private static final boolean foldText = getBooleanSystemProperty("mail.mime.foldtext", true); + private static String defaultJavaCharset; + private static String defaultMIMECharset; + // Tables to map MIME charset names to Java names and vice versa. + // XXX - Should eventually use J2SE 1.4 java.nio.charset.Charset + private static Map mime2java; + private static Map java2mime; + + static { + java2mime = new HashMap<>(40); + mime2java = new HashMap<>(14); + + try { + // Use this class's classloader to load the mapping file + // XXX - we should use SecuritySupport, but it's in another package + InputStream is = + MimeUtility.class.getResourceAsStream( + "/META-INF/javamail.charset.map"); + + if (is != null) { + try { + LineInputStream lineInput = StreamProvider.provider().inputLineStream(is, false); + + // Load the JDK-to-MIME charset mapping table + loadMappings(lineInput, java2mime); + + // Load the MIME-to-JDK charset mapping table + loadMappings(lineInput, mime2java); + } finally { + try { + is.close(); + } catch (Exception cex) { + // ignore + } + } + } + } catch (Exception ex) { + } + + // If we didn't load the tables, e.g., because we didn't have + // permission, load them manually. The entries here should be + // the same as the default javamail.charset.map. + if (java2mime.isEmpty()) { + java2mime.put("8859_1", "ISO-8859-1"); + java2mime.put("iso8859_1", "ISO-8859-1"); + java2mime.put("iso8859-1", "ISO-8859-1"); + + java2mime.put("8859_2", "ISO-8859-2"); + java2mime.put("iso8859_2", "ISO-8859-2"); + java2mime.put("iso8859-2", "ISO-8859-2"); + + java2mime.put("8859_3", "ISO-8859-3"); + java2mime.put("iso8859_3", "ISO-8859-3"); + java2mime.put("iso8859-3", "ISO-8859-3"); + + java2mime.put("8859_4", "ISO-8859-4"); + java2mime.put("iso8859_4", "ISO-8859-4"); + java2mime.put("iso8859-4", "ISO-8859-4"); + + java2mime.put("8859_5", "ISO-8859-5"); + java2mime.put("iso8859_5", "ISO-8859-5"); + java2mime.put("iso8859-5", "ISO-8859-5"); + + java2mime.put("8859_6", "ISO-8859-6"); + java2mime.put("iso8859_6", "ISO-8859-6"); + java2mime.put("iso8859-6", "ISO-8859-6"); + + java2mime.put("8859_7", "ISO-8859-7"); + java2mime.put("iso8859_7", "ISO-8859-7"); + java2mime.put("iso8859-7", "ISO-8859-7"); + + java2mime.put("8859_8", "ISO-8859-8"); + java2mime.put("iso8859_8", "ISO-8859-8"); + java2mime.put("iso8859-8", "ISO-8859-8"); + + java2mime.put("8859_9", "ISO-8859-9"); + java2mime.put("iso8859_9", "ISO-8859-9"); + java2mime.put("iso8859-9", "ISO-8859-9"); + + java2mime.put("sjis", "Shift_JIS"); + java2mime.put("jis", "ISO-2022-JP"); + java2mime.put("iso2022jp", "ISO-2022-JP"); + java2mime.put("euc_jp", "euc-jp"); + java2mime.put("koi8_r", "koi8-r"); + java2mime.put("euc_cn", "euc-cn"); + java2mime.put("euc_tw", "euc-tw"); + java2mime.put("euc_kr", "euc-kr"); + } + if (mime2java.isEmpty()) { + mime2java.put("iso-2022-cn", "ISO2022CN"); + mime2java.put("iso-2022-kr", "ISO2022KR"); + mime2java.put("utf-8", "UTF8"); + mime2java.put("utf8", "UTF8"); + mime2java.put("ja_jp.iso2022-7", "ISO2022JP"); + mime2java.put("ja_jp.eucjp", "EUCJIS"); + mime2java.put("euc-kr", "KSC5601"); + mime2java.put("euckr", "KSC5601"); + mime2java.put("us-ascii", "ISO-8859-1"); + mime2java.put("x-us-ascii", "ISO-8859-1"); + mime2java.put("gb2312", "GB18030"); + mime2java.put("cp936", "GB18030"); + mime2java.put("ms936", "GB18030"); + mime2java.put("gbk", "GB18030"); + } + } + + // This class cannot be instantiated + private MimeUtility() { + } + + /** + * Get the Content-Transfer-Encoding that should be applied + * to the input stream of this DataSource, to make it mail-safe.

+ *

+ * The algorithm used here is:
+ *

    + *
  • + * If the DataSource implements {@link EncodingAware}, ask it + * what encoding to use. If it returns non-null, return that value. + *
  • + * If the primary type of this datasource is "text" and if all + * the bytes in its input stream are US-ASCII, then the encoding + * is StreamProvider.BIT7_ENCODER. If more than half of the bytes are non-US-ASCII, then + * the encoding is StreamProvider.BASE_64_ENCODER. If less than half of the bytes are + * non-US-ASCII, then the encoding is StreamProvider.QUOTED_PRINTABLE_ENCODER. + *
  • + * If the primary type of this datasource is not "text", then if + * all the bytes of its input stream are US-ASCII, the encoding + * is StreamProvider.BIT7_ENCODER. If there is even one non-US-ASCII character, the + * encoding is StreamProvider.BASE_64_ENCODER. + *
+ * + * @param ds the DataSource + * @return the encoding. This is either StreamProvider.BIT7_ENCODER, + * StreamProvider.QUOTED_PRINTABLE_ENCODER or StreamProvider.BASE_64_ENCODER + */ + public static String getEncoding(DataSource ds) { + ContentType cType = null; + InputStream is = null; + String encoding = null; + + if (ds instanceof EncodingAware) { + encoding = ((EncodingAware) ds).getEncoding(); + if (encoding != null) + return encoding; + } + try { + cType = new ContentType(ds.getContentType()); + is = ds.getInputStream(); + + boolean isText = cType.match("text/*"); + // if not text, stop processing when we see non-ASCII + int i = checkAscii(is, ALL, !isText); + switch (i) { + case ALL_ASCII: + encoding = EncoderTypes.BIT7_ENCODER.getEncoder(); // all ASCII + break; + case MOSTLY_ASCII: + if (isText && nonAsciiCharset(cType)) + encoding = EncoderTypes.BASE_64.getEncoder(); // charset isn't compatible with ASCII + else + encoding = EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder(); // mostly ASCII + break; + default: + encoding = EncoderTypes.BASE_64.getEncoder(); // mostly binary + break; + } + + } catch (Exception ex) { + return EncoderTypes.BASE_64.getEncoder(); // what else ?! + } finally { + // Close the input stream + try { + if (is != null) + is.close(); + } catch (IOException ioex) { + } + } + + return encoding; + } + + /** + * Determine whether the charset in the Content-Type is compatible + * with ASCII or not. A charset is compatible with ASCII if the + * encoded byte stream representing the Unicode string "\r\n" is + * the ASCII characters CR and LF. For example, the utf-16be + * charset is not compatible with ASCII. + *

+ * For performance, we keep a static map that caches the results. + */ + private static boolean nonAsciiCharset(ContentType ct) { + String charset = ct.getParameter("charset"); + if (charset == null) + return false; + charset = charset.toLowerCase(Locale.ENGLISH); + Boolean bool; + synchronized (nonAsciiCharsetMap) { + bool = nonAsciiCharsetMap.get(charset); + } + if (bool == null) { + try { + byte[] b = "\r\n".getBytes(charset); + bool = Boolean.valueOf( + b.length != 2 || b[0] != 015 || b[1] != 012); + } catch (UnsupportedEncodingException uex) { + bool = Boolean.FALSE; // a guess + } catch (RuntimeException ex) { + bool = Boolean.TRUE; // one of the weird ones? + } + synchronized (nonAsciiCharsetMap) { + nonAsciiCharsetMap.put(charset, bool); + } + } + return bool.booleanValue(); + } + + /** + * Same as getEncoding(DataSource) except that instead + * of reading the data from an InputStream it uses the + * writeTo method to examine the data. This is more + * efficient in the common case of a DataHandler + * created with an object and a MIME type (for example, a + * "text/plain" String) because all the I/O is done in this + * thread. In the case requiring an InputStream the + * DataHandler uses a thread, a pair of pipe streams, + * and the writeTo method to produce the data. + * + * @param dh the DataHandler + * @return the Content-Transfer-Encoding + * @since JavaMail 1.2 + */ + public static String getEncoding(DataHandler dh) { + ContentType cType = null; + String encoding = null; + + /* + * Try to pick the most efficient means of determining the + * encoding. If this DataHandler was created using a DataSource, + * the getEncoding(DataSource) method is typically faster. If + * the DataHandler was created with an object, this method is + * much faster. To distinguish the two cases, we use a heuristic. + * A DataHandler created with an object will always have a null name. + * A DataHandler created with a DataSource will usually have a + * non-null name. + * + * XXX - This is actually quite a disgusting hack, but it makes + * a common case run over twice as fast. + */ + if (dh.getName() != null) + return getEncoding(dh.getDataSource()); + + try { + cType = new ContentType(dh.getContentType()); + } catch (Exception ex) { + return EncoderTypes.BASE_64.getEncoder(); // what else ?! + } + + if (cType.match("text/*")) { + // Check all of the available bytes + AsciiOutputStream aos = new AsciiOutputStream(false, false); + try { + dh.writeTo(aos); + } catch (IOException ex) { + // ignore it, can't happen + } + switch (aos.getAscii()) { + case ALL_ASCII: + encoding = EncoderTypes.BIT7_ENCODER.getEncoder(); // all ascii + break; + case MOSTLY_ASCII: + encoding = EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder(); // mostly ascii + break; + default: + encoding = EncoderTypes.BASE_64.getEncoder(); // mostly binary + break; + } + } else { // not "text" + // Check all of available bytes, break out if we find + // at least one non-US-ASCII character + AsciiOutputStream aos = + new AsciiOutputStream(true, encodeEolStrict); + try { + dh.writeTo(aos); + } catch (IOException ex) { + } // ignore it + if (aos.getAscii() == ALL_ASCII) // all ascii + encoding = EncoderTypes.BIT7_ENCODER.getEncoder(); + else // found atleast one non-ascii character, use b64 + encoding = EncoderTypes.BASE_64.getEncoder(); + } + + return encoding; + } + + /** + * Decode the given input stream. The Input stream returned is + * the decoded input stream. All the encodings defined in RFC 2045 + * are supported here. They include StreamProvider.BASE_64_ENCODER, StreamProvider.QUOTED_PRINTABLE_ENCODER, + * StreamProvider.BIT7_ENCODER, StreamProvider.BIT8_ENCODER, and StreamProvider.BINARY_ENCODER. In addition, StreamProvider.UU_ENCODER is also + * supported.

+ *

+ * In the current implementation, if the + * mail.mime.ignoreunknownencoding system property is set to + * "true", unknown encoding values are ignored and the + * original InputStream is returned. + * + * @param is input stream + * @param encoding the encoding of the stream. + * @return decoded input stream. + * @throws MessagingException if the encoding is unknown + */ + public static InputStream decode(InputStream is, String encoding) + throws MessagingException { + if (encoding.equalsIgnoreCase(EncoderTypes.BASE_64.getEncoder())) + return StreamProvider.provider().inputBase64(is); + else if (encoding.equalsIgnoreCase(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder())) + return StreamProvider.provider().inputQP(is); + else if (encoding.equalsIgnoreCase(EncoderTypes.UU_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.X_UU_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.X_UUE.getEncoder())) + return StreamProvider.provider().inputUU(is); + else if (encoding.equalsIgnoreCase(EncoderTypes.BINARY_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.BIT7_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.BIT8_ENCODER.getEncoder())) + return StreamProvider.provider().inputBinary(is); + else { + if (!ignoreUnknownEncoding) + throw new MessagingException("Unknown encoding: " + encoding); + return is; + } + } + + /** + * Wrap an encoder around the given output stream. + * All the encodings defined in RFC 2045 are supported here. + * They include StreamProvider.BASE_64_ENCODER, StreamProvider.QUOTED_PRINTABLE_ENCODER, StreamProvider.BIT7_ENCODER, StreamProvider.BIT8_ENCODER and + * StreamProvider.BINARY_ENCODER. In addition, StreamProvider.UU_ENCODER is also supported. + * + * @param os output stream + * @param encoding the encoding of the stream. + * @return output stream that applies the + * specified encoding. + * @throws MessagingException if the encoding is unknown + */ + public static OutputStream encode(OutputStream os, String encoding) + throws MessagingException { + if (encoding == null) + return os; + else if (encoding.equalsIgnoreCase(EncoderTypes.BASE_64.getEncoder())) + return StreamProvider.provider().outputBase64(os); + else if (encoding.equalsIgnoreCase(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder())) + return StreamProvider.provider().outputQP(os); + else if (encoding.equalsIgnoreCase(EncoderTypes.UU_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.X_UU_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.X_UUE.getEncoder())) + return StreamProvider.provider().outputUU(os, null); + else if (encoding.equalsIgnoreCase(EncoderTypes.BINARY_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.BIT7_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.BIT8_ENCODER.getEncoder())) + return StreamProvider.provider().outputBinary(os); + else + throw new MessagingException("Unknown encoding: " + encoding); + } + + /** + * Wrap an encoder around the given output stream. + * All the encodings defined in RFC 2045 are supported here. + * They include StreamProvider.BASE_64_ENCODER, StreamProvider.QUOTED_PRINTABLE_ENCODER, StreamProvider.BIT7_ENCODER, StreamProvider.BIT8_ENCODER and + * StreamProvider.BINARY_ENCODER. In addition, StreamProvider.UU_ENCODER is also supported. + * The filename parameter is used with the StreamProvider.UU_ENCODER + * encoding and is included in the encoded output. + * + * @param os output stream + * @param encoding the encoding of the stream. + * @param filename name for the file being encoded (only used + * with uuencode) + * @return output stream that applies the + * specified encoding. + * @throws MessagingException for unknown encodings + * @since JavaMail 1.2 + */ + public static OutputStream encode(OutputStream os, String encoding, + String filename) + throws MessagingException { + if (encoding == null) + return os; + else if (encoding.equalsIgnoreCase(EncoderTypes.BASE_64.getEncoder())) + return StreamProvider.provider().outputBase64(os); + else if (encoding.equalsIgnoreCase(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder())) + return StreamProvider.provider().outputQP(os); + else if (encoding.equalsIgnoreCase(EncoderTypes.UU_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.X_UU_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.X_UUE.getEncoder())) + return StreamProvider.provider().outputUU(os, filename); + else if (encoding.equalsIgnoreCase(EncoderTypes.BINARY_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.BIT7_ENCODER.getEncoder()) || + encoding.equalsIgnoreCase(EncoderTypes.BIT8_ENCODER.getEncoder())) + return StreamProvider.provider().outputBinary(os); + else + throw new MessagingException("Unknown encoding: " + encoding); + } + + /** + * Encode a RFC 822 "text" token into mail-safe form as per + * RFC 2047.

+ *

+ * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the platform's + * default charset, then transfer-encoded using either the B or + * Q encoding. The resulting bytes are then returned as a Unicode + * string containing only ASCII characters.

+ *

+ * Note that this method should be used to encode only + * "unstructured" RFC 822 headers.

+ *

+ * Example of usage: + *

+     *
+     *  MimePart part = ...
+     *  String rawvalue = "FooBar Mailer, Japanese version 1.1"
+     *  try {
+     *    // If we know for sure that rawvalue contains only US-ASCII
+     *    // characters, we can skip the encoding part
+     *    part.setHeader("X-mailer", MimeUtility.encodeText(rawvalue));
+     *  } catch (UnsupportedEncodingException e) {
+     *    // encoding failure
+     *  } catch (MessagingException me) {
+     *   // setHeader() failure
+     *  }
+     *
+     * 
+ * + * @param text Unicode string + * @return Unicode string containing only US-ASCII characters + * @throws UnsupportedEncodingException if the encoding fails + */ + public static String encodeText(String text) + throws UnsupportedEncodingException { + return encodeText(text, null, null); + } + + /** + * Encode a RFC 822 "text" token into mail-safe form as per + * RFC 2047.

+ *

+ * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the specified + * charset, then transfer-encoded using either the B or Q encoding. + * The resulting bytes are then returned as a Unicode string + * containing only ASCII characters.

+ *

+ * Note that this method should be used to encode only + * "unstructured" RFC 822 headers. + * + * @param text the header value + * @param charset the charset. If this parameter is null, the + * platform's default chatset is used. + * @param encoding the encoding to be used. Currently supported + * values are "B" and "Q". If this parameter is null, then + * the "Q" encoding is used if most of characters to be + * encoded are in the ASCII charset, otherwise "B" encoding + * is used. + * @return Unicode string containing only US-ASCII characters + * @throws UnsupportedEncodingException if the charset + * conversion failed. + */ + public static String encodeText(String text, String charset, + String encoding) + throws UnsupportedEncodingException { + return encodeWord(text, charset, encoding, false); + } + + /** + * Decode "unstructured" headers, that is, headers that are defined + * as '*text' as per RFC 822.

+ *

+ * The string is decoded using the algorithm specified in + * RFC 2047, Section 6.1. If the charset-conversion fails + * for any sequence, an UnsupportedEncodingException is thrown. + * If the String is not an RFC 2047 style encoded header, it is + * returned as-is

+ *

+ * Example of usage: + *

+     *
+     *  MimePart part = ...
+     *  String rawvalue = null;
+     *  String  value = null;
+     *  try {
+     *    if ((rawvalue = part.getHeader("X-mailer")[0]) != null)
+     *      value = MimeUtility.decodeText(rawvalue);
+     *  } catch (UnsupportedEncodingException e) {
+     *      // Don't care
+     *      value = rawvalue;
+     *  } catch (MessagingException me) { }
+     *
+     *  return value;
+     *
+     * 
+ * + * @param etext the possibly encoded value + * @return the decoded text + * @throws UnsupportedEncodingException if the charset + * conversion failed. + */ + public static String decodeText(String etext) + throws UnsupportedEncodingException { + /* + * We look for sequences separated by "linear-white-space". + * (as per RFC 2047, Section 6.1) + * RFC 822 defines "linear-white-space" as SPACE | HT | CR | NL. + */ + String lwsp = " \t\n\r"; + StringTokenizer st; + + /* + * First, lets do a quick run thru the string and check + * whether the sequence "=?" exists at all. If none exists, + * we know there are no encoded-words in here and we can just + * return the string as-is, without suffering thru the later + * decoding logic. + * This handles the most common case of unencoded headers + * efficiently. + */ + if (!etext.contains("=?")) + return etext; + + // Encoded words found. Start decoding ... + + st = new StringTokenizer(etext, lwsp, true); + StringBuilder sb = new StringBuilder(); // decode buffer + StringBuilder wsb = new StringBuilder(); // white space buffer + boolean prevWasEncoded = false; + + while (st.hasMoreTokens()) { + char c; + String s = st.nextToken(); + // If whitespace, append it to the whitespace buffer + if (((c = s.charAt(0)) == ' ') || (c == '\t') || + (c == '\r') || (c == '\n')) + wsb.append(c); + else { + // Check if token is an 'encoded-word' .. + String word; + try { + word = decodeWord(s); + // Yes, this IS an 'encoded-word'. + if (!prevWasEncoded && wsb.length() > 0) { + // if the previous word was also encoded, we + // should ignore the collected whitespace. Else + // we include the whitespace as well. + sb.append(wsb); + } + prevWasEncoded = true; + } catch (ParseException pex) { + // This is NOT an 'encoded-word'. + word = s; + // possibly decode inner encoded words + if (!decodeStrict) { + String dword = decodeInnerWords(word); + if (dword != word) { + // if a different String object was returned, + // decoding was done. + if (prevWasEncoded && word.startsWith("=?")) { + // encoded followed by encoded, + // throw away whitespace between + } else { + // include collected whitespace .. + if (wsb.length() > 0) + sb.append(wsb); + } + // did original end with encoded? + prevWasEncoded = word.endsWith("?="); + word = dword; + } else { + // include collected whitespace .. + if (wsb.length() > 0) + sb.append(wsb); + prevWasEncoded = false; + } + } else { + // include collected whitespace .. + if (wsb.length() > 0) + sb.append(wsb); + prevWasEncoded = false; + } + } + sb.append(word); // append the actual word + wsb.setLength(0); // reset wsb for reuse + } + } + sb.append(wsb); // append trailing whitespace + return sb.toString(); + } + + /** + * Encode a RFC 822 "word" token into mail-safe form as per + * RFC 2047.

+ *

+ * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the platform's + * default charset, then transfer-encoded using either the B or + * Q encoding. The resulting bytes are then returned as a Unicode + * string containing only ASCII characters.

+ *

+ * This method is meant to be used when creating RFC 822 "phrases". + * The InternetAddress class, for example, uses this to encode + * it's 'phrase' component. + * + * @param word Unicode string + * @return Array of Unicode strings containing only US-ASCII + * characters. + * @throws UnsupportedEncodingException if the encoding fails + */ + public static String encodeWord(String word) + throws UnsupportedEncodingException { + return encodeWord(word, null, null); + } + + /** + * Encode a RFC 822 "word" token into mail-safe form as per + * RFC 2047.

+ *

+ * The given Unicode string is examined for non US-ASCII + * characters. If the string contains only US-ASCII characters, + * it is returned as-is. If the string contains non US-ASCII + * characters, it is first character-encoded using the specified + * charset, then transfer-encoded using either the B or Q encoding. + * The resulting bytes are then returned as a Unicode string + * containing only ASCII characters. + * + * @param word Unicode string + * @param charset the MIME charset + * @param encoding the encoding to be used. Currently supported + * values are "B" and "Q". If this parameter is null, then + * the "Q" encoding is used if most of characters to be + * encoded are in the ASCII charset, otherwise "B" encoding + * is used. + * @return Unicode string containing only US-ASCII characters + * @throws UnsupportedEncodingException if the encoding fails + */ + public static String encodeWord(String word, String charset, + String encoding) + throws UnsupportedEncodingException { + return encodeWord(word, charset, encoding, true); + } + + /* + * Encode the given string. The parameter 'encodingWord' should + * be true if a RFC 822 "word" token is being encoded and false if a + * RFC 822 "text" token is being encoded. This is because the + * "Q" encoding defined in RFC 2047 has more restrictions when + * encoding "word" tokens. (Sigh) + */ + private static String encodeWord(String string, String charset, + String encoding, boolean encodingWord) + throws UnsupportedEncodingException { + + // If 'string' contains only US-ASCII characters, just + // return it. + int ascii = checkAscii(string); + if (ascii == ALL_ASCII) + return string; + + // Else, apply the specified charset conversion. + String jcharset; + if (charset == null) { // use default charset + jcharset = getDefaultJavaCharset(); // the java charset + charset = getDefaultMIMECharset(); // the MIME equivalent + } else // MIME charset -> java charset + jcharset = javaCharset(charset); + + // If no transfer-encoding is specified, figure one out. + if (encoding == null) { + if (ascii != MOSTLY_NONASCII) + encoding = "Q"; + else + encoding = "B"; + } + + boolean b64; + if (encoding.equalsIgnoreCase("B")) + b64 = true; + else if (encoding.equalsIgnoreCase("Q")) + b64 = false; + else + throw new UnsupportedEncodingException( + "Unknown transfer encoding: " + encoding); + + StringBuilder outb = new StringBuilder(); // the output buffer + doEncode(string, b64, jcharset, + // As per RFC 2047, size of an encoded string should not + // exceed 75 bytes. + // 7 = size of "=?", '?', 'B'/'Q', '?', "?=" + 75 - 7 - charset.length(), // the available space + "=?" + charset + "?" + encoding + "?", // prefix + true, encodingWord, outb); + + return outb.toString(); + } + + /** + * Returns the length of the encoded version of this byte array. + * + * @param b the byte array + * @return the length + */ + private static int bEncodedLength(byte[] b) { + return ((b.length + 2) / 3) * 4; + } + + /** + * 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 + */ + private static int qEncodedLength(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; + } + + private static void doEncode(String string, boolean b64, + String jcharset, int avail, String prefix, + boolean first, boolean encodingWord, StringBuilder buf) + throws UnsupportedEncodingException { + + // First find out what the length of the encoded version of + // 'string' would be. + byte[] bytes = string.getBytes(jcharset); + int len; + if (b64) // "B" encoding + len = bEncodedLength(bytes); + else // "Q" + len = qEncodedLength(bytes, encodingWord); + + int size; + if ((len > avail) && ((size = string.length()) > 1)) { + // If the length is greater than 'avail', split 'string' + // into two and recurse. + // Have to make sure not to split a Unicode surrogate pair. + int split = size / 2; + if (Character.isHighSurrogate(string.charAt(split - 1))) + split--; + if (split > 0) + doEncode(string.substring(0, split), b64, jcharset, + avail, prefix, first, encodingWord, buf); + doEncode(string.substring(split, size), b64, jcharset, + avail, prefix, false, encodingWord, buf); + } else { + // length <= than 'avail'. Encode the given string + ByteArrayOutputStream os = new ByteArrayOutputStream(); + OutputStream eos; // the encoder + if (b64) { // "B" encoding + eos = StreamProvider.provider().outputB(os); + } else { // "Q" encoding + eos = StreamProvider.provider().outputQ(os, encodingWord); + } + + try { // do the encoding + eos.write(bytes); + eos.close(); + } catch (IOException ioex) { + } + + byte[] encodedBytes = os.toByteArray(); // the encoded stuff + // Now write out the encoded (all ASCII) bytes into our + // StringBuilder + if (!first) // not the first line of this sequence + if (foldEncodedWords) + buf.append("\r\n "); // start a continuation line + else + buf.append(" "); // line will be folded later + + buf.append(prefix); + for (int i = 0; i < encodedBytes.length; i++) + buf.append((char) encodedBytes[i]); + buf.append("?="); // terminate the current sequence + } + } + + /** + * The string is parsed using the rules in RFC 2047 and RFC 2231 for + * parsing an "encoded-word". If the parse fails, a ParseException is + * thrown. Otherwise, it is transfer-decoded, and then + * charset-converted into Unicode. If the charset-conversion + * fails, an UnsupportedEncodingException is thrown. + * + * @param eword the encoded value + * @return the decoded word + * @throws ParseException if the string is not an + * encoded-word as per RFC 2047 and RFC 2231. + * @throws UnsupportedEncodingException if the charset + * conversion failed. + */ + public static String decodeWord(String eword) + throws ParseException, UnsupportedEncodingException { + + if (!eword.startsWith("=?")) // not an encoded word + throw new ParseException( + "encoded word does not start with \"=?\": " + eword); + + // get charset + int start = 2; + int pos; + if ((pos = eword.indexOf('?', start)) == -1) + throw new ParseException( + "encoded word does not include charset: " + eword); + String charset = eword.substring(start, pos); + int lpos = charset.indexOf('*'); // RFC 2231 language specified? + if (lpos >= 0) // yes, throw it away + charset = charset.substring(0, lpos); + charset = javaCharset(charset); + + // get encoding + start = pos + 1; + if ((pos = eword.indexOf('?', start)) == -1) + throw new ParseException( + "encoded word does not include encoding: " + eword); + String encoding = eword.substring(start, pos); + + // get encoded-sequence + start = pos + 1; + if ((pos = eword.indexOf("?=", start)) == -1) + throw new ParseException( + "encoded word does not end with \"?=\": " + eword); + /* + * XXX - should include this, but leaving it out for compatibility... + * + if (decodeStrict && pos != eword.length() - 2) + throw new ParseException( + "encoded word does not end with \"?=\": " + eword);); + */ + String word = eword.substring(start, pos); + + try { + String decodedWord; + if (word.length() > 0) { + // Extract the bytes from word + ByteArrayInputStream bis = + new ByteArrayInputStream(getBytes(word)); + + // Get the appropriate decoder + InputStream is; + if (encoding.equalsIgnoreCase("B")) + is = StreamProvider.provider().inputBase64(bis); + else if (encoding.equalsIgnoreCase("Q")) + is = StreamProvider.provider().inputQ(bis); + else + throw new UnsupportedEncodingException( + "unknown encoding: " + encoding); + + // For b64 & q, size of decoded word <= size of word. So + // the decoded bytes must fit into the 'bytes' array. This + // is certainly more efficient than writing bytes into a + // ByteArrayOutputStream and then pulling out the byte[] + // from it. + int count = bis.available(); + byte[] bytes = new byte[count]; + // count is set to the actual number of decoded bytes + count = is.read(bytes, 0, count); + + // Finally, convert the decoded bytes into a String using + // the specified charset + decodedWord = count <= 0 ? "" : + new String(bytes, 0, count, charset); + } else { + // no characters to decode, return empty string + decodedWord = ""; + } + if (pos + 2 < eword.length()) { + // there's still more text in the string + String rest = eword.substring(pos + 2); + if (!decodeStrict) + rest = decodeInnerWords(rest); + decodedWord += rest; + } + return decodedWord; + } catch (UnsupportedEncodingException uex) { + // explicitly catch and rethrow this exception, otherwise + // the below IOException catch will swallow this up! + throw uex; + } catch (IOException ioex) { + // Shouldn't happen. + throw new ParseException(ioex.toString()); + } catch (IllegalArgumentException iex) { + /* An unknown charset of the form ISO-XXX-XXX, will cause + * the JDK to throw an IllegalArgumentException ... Since 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(charset); + } + } + + /** + * Look for encoded words within a word. The MIME spec doesn't + * allow this, but many broken mailers, especially Japanese mailers, + * produce such incorrect encodings. + */ + private static String decodeInnerWords(String word) + throws UnsupportedEncodingException { + int start = 0, i; + StringBuilder buf = new StringBuilder(); + while ((i = word.indexOf("=?", start)) >= 0) { + buf.append(word, start, i); + // find first '?' after opening '=?' - end of charset + int end = word.indexOf('?', i + 2); + if (end < 0) + break; + // find next '?' after that - end of encoding + end = word.indexOf('?', end + 1); + if (end < 0) + break; + // find terminating '?=' + end = word.indexOf("?=", end + 1); + if (end < 0) + break; + String s = word.substring(i, end + 2); + try { + s = decodeWord(s); + } catch (ParseException pex) { + // ignore it, just use the original string + } + buf.append(s); + start = end + 2; + } + if (start == 0) + return word; + if (start < word.length()) + buf.append(word.substring(start)); + return buf.toString(); + } + + /** + * A utility method to quote a word, if the word contains any + * characters from the specified 'specials' list.

+ *

+ * The HeaderTokenizer class defines two special + * sets of delimiters - MIME and RFC 822.

+ *

+ * This method is typically used during the generation of + * RFC 822 and MIME header fields. + * + * @param word word to be quoted + * @param specials the set of special characters + * @return the possibly quoted word + * @see HeaderTokenizer#MIME + * @see HeaderTokenizer#RFC822 + */ + public static String quote(String word, String specials) { + int len = word == null ? 0 : word.length(); + if (len == 0) + return "\"\""; // an empty string is handled specially + + /* + * Look for any "bad" characters, Escape and + * quote the entire string if necessary. + */ + boolean needQuoting = false; + for (int i = 0; i < len; i++) { + char c = word.charAt(i); + if (c == '"' || c == '\\' || c == '\r' || c == '\n') { + // need to escape them and then quote the whole string + StringBuilder sb = new StringBuilder(len + 3); + sb.append('"'); + sb.append(word, 0, i); + int lastc = 0; + for (int j = i; j < len; j++) { + char cc = word.charAt(j); + if ((cc == '"') || (cc == '\\') || + (cc == '\r') || (cc == '\n')) + if (cc == '\n' && lastc == '\r') + ; // do nothing, CR was already escaped + else + sb.append('\\'); // Escape the character + sb.append(cc); + lastc = cc; + } + sb.append('"'); + return sb.toString(); + } else if (c < 040 || (c >= 0177 && !allowUtf8) || + specials.indexOf(c) >= 0) + // These characters cause the string to be quoted + needQuoting = true; + } + + if (needQuoting) { + StringBuilder sb = new StringBuilder(len + 2); + sb.append('"').append(word).append('"'); + return sb.toString(); + } else + return word; + } + + /** + * Fold a string at linear whitespace so that each line is no longer + * than 76 characters, if possible. If there are more than 76 + * non-whitespace characters consecutively, the string is folded at + * the first whitespace after that sequence. The parameter + * used indicates how many characters have been used in + * the current line; it is usually the length of the header name.

+ *

+ * Note that line breaks in the string aren't escaped; they probably + * should be. + * + * @param used characters used in line so far + * @param s the string to fold + * @return the folded string + * @since JavaMail 1.4 + */ + public static String fold(int used, String s) { + if (!foldText) + return s; + + int end; + char c; + // Strip trailing spaces and newlines + for (end = s.length() - 1; end >= 0; end--) { + c = s.charAt(end); + if (c != ' ' && c != '\t' && c != '\r' && c != '\n') + break; + } + if (end != s.length() - 1) + s = s.substring(0, end + 1); + + // if the string fits now, just return it + if (used + s.length() <= 76) + return makesafe(s); + + // have to actually fold the string + StringBuilder sb = new StringBuilder(s.length() + 4); + char lastc = 0; + while (used + s.length() > 76) { + int lastspace = -1; + for (int i = 0; i < s.length(); i++) { + if (lastspace != -1 && used + i > 76) + break; + c = s.charAt(i); + if (c == ' ' || c == '\t') + if (!(lastc == ' ' || lastc == '\t')) + lastspace = i; + lastc = c; + } + if (lastspace == -1) { + // no space, use the whole thing + sb.append(s); + s = ""; + used = 0; + break; + } + sb.append(s, 0, lastspace); + sb.append("\r\n"); + lastc = s.charAt(lastspace); + sb.append(lastc); + s = s.substring(lastspace + 1); + used = 1; + } + sb.append(s); + return makesafe(sb); + } + + /** + * If the String or StringBuilder has any embedded newlines, + * make sure they're followed by whitespace, to prevent header + * injection errors. + */ + private static String makesafe(CharSequence s) { + int i; + for (i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\r' || c == '\n') + break; + } + if (i == s.length()) // went through whole string with no CR or LF + return s.toString(); + + // read the lines in the string and reassemble them, + // eliminating blank lines and inserting whitespace as necessary + StringBuilder sb = new StringBuilder(s.length() + 1); + BufferedReader r = new BufferedReader(new StringReader(s.toString())); + String line; + try { + while ((line = r.readLine()) != null) { + if (line.trim().isEmpty()) + continue; + if (!sb.isEmpty()) { + sb.append("\r\n"); + if (line.isEmpty()) { + throw new AssertionError(); + } + char c = line.charAt(0); + if (c != ' ' && c != '\t') + sb.append(' '); + } + sb.append(line); + } + } catch (IOException ex) { + // XXX - should never happen when reading from a string + return s.toString(); + } + return sb.toString(); + } + + /** + * Unfold a folded header. Any line breaks that aren't escaped and + * are followed by whitespace are removed. + * + * @param s the string to unfold + * @return the unfolded string + * @since JavaMail 1.4 + */ + public static String unfold(String s) { + if (!foldText) + return s; + + StringBuilder sb = null; + int i; + while ((i = indexOfAny(s, "\r\n")) >= 0) { + int start = i; + int slen = s.length(); + i++; // skip CR or NL + if (i < slen && s.charAt(i - 1) == '\r' && s.charAt(i) == '\n') + i++; // skip LF + if (start > 0 && s.charAt(start - 1) == '\\') { + // there's a backslash before the line break + // strip it out, but leave in the line break + if (sb == null) + sb = new StringBuilder(s.length()); + sb.append(s, 0, start - 1); + sb.append(s, start, i); + s = s.substring(i); + } else { + char c; + // if next line starts with whitespace, + // or at the end of the string, remove the line break + // XXX - next line should always start with whitespace + if (i >= slen || (c = s.charAt(i)) == ' ' || c == '\t') { + if (sb == null) + sb = new StringBuilder(s.length()); + sb.append(s, 0, start); + s = s.substring(i); + } else { + // it's not a continuation line, just leave in the newline + if (sb == null) + sb = new StringBuilder(s.length()); + sb.append(s, 0, i); + s = s.substring(i); + } + } + } + if (sb != null) { + sb.append(s); + return sb.toString(); + } else + return s; + } + + /** + * Return the first index of any of the characters in "any" in "s", + * or -1 if none are found. + *

+ * This should be a method on String. + */ + private static int indexOfAny(String s, String any) { + return indexOfAny(s, any, 0); + } + + private static int indexOfAny(String s, String any, int start) { + try { + int len = s.length(); + for (int i = start; i < len; i++) { + if (any.indexOf(s.charAt(i)) >= 0) + return i; + } + return -1; + } catch (StringIndexOutOfBoundsException e) { + return -1; + } + } + + /** + * Convert a MIME charset name into a valid Java charset name. + * + * @param charset the MIME charset name + * @return the Java charset equivalent. If a suitable mapping is + * not available, the passed in charset is itself returned. + */ + public static String javaCharset(String charset) { + if (mime2java == null || charset == null) + // no mapping table, or charset parameter is null + return charset; + + String alias = mime2java.get(charset.toLowerCase(Locale.ENGLISH)); + if (alias != null) { + // verify that the mapped name is valid before trying to use it + try { + Charset.forName(alias); + } catch (Exception ex) { + alias = null; // charset alias not valid, use original name + } + } + return alias == null ? charset : alias; + } + + /** + * Convert a java charset into its MIME charset name.

+ *

+ * Note that a future version of JDK (post 1.2) might provide + * this functionality, in which case, we may deprecate this + * method then. + * + * @param charset the JDK charset + * @return the MIME/IANA equivalent. If a mapping + * is not possible, the passed in charset itself + * is returned. + * @since JavaMail 1.1 + */ + public static String mimeCharset(String charset) { + if (java2mime == null || charset == null) + // no mapping table or charset param is null + return charset; + + String alias = java2mime.get(charset.toLowerCase(Locale.ENGLISH)); + return alias == null ? charset : alias; + } + + /** + * Get the default charset corresponding to the system's current + * default locale. If the System property mail.mime.charset + * is set, a system charset corresponding to this MIME charset will be + * returned. + * + * @return the default charset of the system's default locale, + * as a Java charset. (NOT a MIME charset) + * @since JavaMail 1.1 + */ + public static String getDefaultJavaCharset() { + if (defaultJavaCharset == null) { + /* + * If mail.mime.charset is set, it controls the default + * Java charset as well. + */ + String mimecs = System.getProperty("mail.mime.charset"); + if (mimecs != null && !mimecs.isEmpty()) { + defaultJavaCharset = javaCharset(mimecs); + return defaultJavaCharset; + } + defaultJavaCharset = System.getProperty("file.encoding", + "8859_1"); + } + return defaultJavaCharset; + } + + /* + * Get the default MIME charset for this locale. + */ + static String getDefaultMIMECharset() { + if (defaultMIMECharset == null) { + defaultMIMECharset = System.getProperty("mail.mime.charset"); + } + if (defaultMIMECharset == null) + defaultMIMECharset = mimeCharset(getDefaultJavaCharset()); + return defaultMIMECharset; + } + + private static void loadMappings(LineInputStream is, + Map table) { + String currLine; + + while (true) { + try { + currLine = is.readLine(); + } catch (IOException ioex) { + break; // error in reading, stop + } + + if (currLine == null) // end of file, stop + break; + if (currLine.startsWith("--") && currLine.endsWith("--")) + // end of this table + break; + + // ignore empty lines and comments + if (currLine.trim().length() == 0 || currLine.startsWith("#")) + continue; + + // A valid entry is of the form + // where, := SPACE | HT. Parse this + StringTokenizer tk = new StringTokenizer(currLine, " \t"); + try { + String key = tk.nextToken(); + String value = tk.nextToken(); + table.put(key.toLowerCase(Locale.ENGLISH), value); + } catch (NoSuchElementException nex) { + } + } + } + + /** + * Check if the given string contains non US-ASCII characters. + * + * @param s string + * @return ALL_ASCII if all characters in the string + * belong to the US-ASCII charset. MOSTLY_ASCII + * if more than half of the available characters + * are US-ASCII characters. Else MOSTLY_NONASCII. + */ + static int checkAscii(String s) { + int ascii = 0, non_ascii = 0; + int l = s.length(); + + for (int i = 0; i < l; i++) { + if (nonascii(s.charAt(i))) // non-ascii + non_ascii++; + else + ascii++; + } + + if (non_ascii == 0) + return ALL_ASCII; + if (ascii > non_ascii) + return MOSTLY_ASCII; + + return MOSTLY_NONASCII; + } + + /** + * Check if the given byte array contains non US-ASCII characters. + * + * @param b byte array + * @return ALL_ASCII if all characters in the string + * belong to the US-ASCII charset. MOSTLY_ASCII + * if more than half of the available characters + * are US-ASCII characters. Else MOSTLY_NONASCII. + *

+ * XXX - this method is no longer used + */ + static int checkAscii(byte[] b) { + int ascii = 0, non_ascii = 0; + + for (int i = 0; i < b.length; i++) { + // The '&' operator automatically causes b[i] to be promoted + // to an int, and we mask out the higher bytes in the int + // so that the resulting value is not a negative integer. + if (nonascii(b[i] & 0xff)) // non-ascii + non_ascii++; + else + ascii++; + } + + if (non_ascii == 0) + return ALL_ASCII; + if (ascii > non_ascii) + return MOSTLY_ASCII; + + return MOSTLY_NONASCII; + } + + /** + * Check if the given input stream contains non US-ASCII characters. + * Upto max bytes are checked. If max is + * set to ALL, then all the bytes available in this + * input stream are checked. If breakOnNonAscii is true + * the check terminates when the first non-US-ASCII character is + * found and MOSTLY_NONASCII is returned. Else, the check continues + * till max bytes or till the end of stream. + * + * @param is the input stream + * @param max maximum bytes to check for. The special value + * ALL indicates that all the bytes in this input + * stream must be checked. + * @param breakOnNonAscii if true, then terminate the + * the check when the first non-US-ASCII character + * is found. + * @return ALL_ASCII if all characters in the string + * belong to the US-ASCII charset. MOSTLY_ASCII + * if more than half of the available characters + * are US-ASCII characters. Else MOSTLY_NONASCII. + */ + static int checkAscii(InputStream is, int max, boolean breakOnNonAscii) { + int ascii = 0, non_ascii = 0; + int len; + int block = 4096; + int linelen = 0; + boolean longLine = false, badEOL = false; + boolean checkEOL = encodeEolStrict && breakOnNonAscii; + byte[] buf = null; + if (max != 0) { + block = (max == ALL) ? 4096 : Math.min(max, 4096); + buf = new byte[block]; + } + while (max != 0) { + try { + if ((len = is.read(buf, 0, block)) == -1) + break; + int lastb = 0; + for (int i = 0; i < len; i++) { + // The '&' operator automatically causes b[i] to + // be promoted to an int, and we mask out the higher + // bytes in the int so that the resulting value is + // not a negative integer. + int b = buf[i] & 0xff; + if (checkEOL && + ((lastb == '\r' && b != '\n') || + (lastb != '\r' && b == '\n'))) + badEOL = true; + if (b == '\r' || b == '\n') + linelen = 0; + else { + linelen++; + if (linelen > 998) // 1000 - CRLF + longLine = true; + } + if (nonascii(b)) { // non-ascii + if (breakOnNonAscii) // we are done + return MOSTLY_NONASCII; + else + non_ascii++; + } else + ascii++; + lastb = b; + } + } catch (IOException ioex) { + break; + } + if (max != ALL) + max -= len; + } + + if (max == 0 && breakOnNonAscii) + // We have been told to break on the first non-ascii character. + // We haven't got any non-ascii character yet, but then we + // have not checked all of the available bytes either. So we + // cannot say for sure that this input stream is ALL_ASCII, + // and hence we must play safe and return MOSTLY_NONASCII + + return MOSTLY_NONASCII; + + if (non_ascii == 0) { // no non-us-ascii characters so far + // If we're looking at non-text data, and we saw CR without LF + // or vice versa, consider this mostly non-ASCII so that it + // will be base64 encoded (since the quoted-printable encoder + // doesn't encode this case properly). + if (badEOL) + return MOSTLY_NONASCII; + // if we've seen a long line, we degrade to mostly ascii + else if (longLine) + return MOSTLY_ASCII; + else + return ALL_ASCII; + } + if (ascii > non_ascii) // mostly ascii + return MOSTLY_ASCII; + return MOSTLY_NONASCII; + } + + static final boolean nonascii(int b) { + return b >= 0177 || (b < 040 && b != '\r' && b != '\n' && b != '\t'); + } + + // This is a copy of ASCIIUtility#getBytes that was moved to implementation module + 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; + } + + // This is a copy of ASCIIUtility#getBytes that was moved to implementation module + 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; + } + + /** + * 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 + */ + static boolean getBooleanProperty(Properties props, String name, boolean def) { + return getBoolean(getProp(props, 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 + */ + static boolean getBooleanSystemProperty(String name, boolean def) { + return getBoolean(getProp(System.getProperties(), name), 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 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; + return def; + } +} + +/** + * An OutputStream that determines whether the data written to + * it is all ASCII, mostly ASCII, or mostly non-ASCII. + */ +class AsciiOutputStream extends OutputStream { + private boolean breakOnNonAscii; + private int ascii = 0, non_ascii = 0; + private int linelen = 0; + private boolean longLine = false; + private boolean badEOL = false; + private boolean checkEOL = false; + private int lastb = 0; + private int ret = 0; + + public AsciiOutputStream(boolean breakOnNonAscii, boolean encodeEolStrict) { + this.breakOnNonAscii = breakOnNonAscii; + checkEOL = encodeEolStrict && breakOnNonAscii; + } + + @Override + public void write(int b) throws IOException { + check(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 { + len += off; + for (int i = off; i < len; i++) + check(b[i]); + } + + private final void check(int b) throws IOException { + b &= 0xff; + if (checkEOL && + ((lastb == '\r' && b != '\n') || (lastb != '\r' && b == '\n'))) + badEOL = true; + if (b == '\r' || b == '\n') + linelen = 0; + else { + linelen++; + if (linelen > 998) // 1000 - CRLF + longLine = true; + } + if (MimeUtility.nonascii(b)) { // non-ascii + non_ascii++; + if (breakOnNonAscii) { // we are done + ret = MimeUtility.MOSTLY_NONASCII; + throw new EOFException(); + } + } else + ascii++; + lastb = b; + } + + /** + * Return ASCII-ness of data stream. + */ + public int getAscii() { + if (ret != 0) + return ret; + // If we're looking at non-text data, and we saw CR without LF + // or vice versa, consider this mostly non-ASCII so that it + // will be base64 encoded (since the quoted-printable encoder + // doesn't encode this case properly). + if (badEOL) + return MimeUtility.MOSTLY_NONASCII; + else if (non_ascii == 0) { // no non-us-ascii characters so far + // if we've seen a long line, we degrade to mostly ascii + if (longLine) + return MimeUtility.MOSTLY_ASCII; + else + return MimeUtility.ALL_ASCII; + } + if (ascii > non_ascii) // mostly ascii + return MimeUtility.MOSTLY_ASCII; + return MimeUtility.MOSTLY_NONASCII; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/NewsAddress.java b/net-mail/src/main/java/jakarta/mail/internet/NewsAddress.java new file mode 100644 index 0000000..d67f3ab --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/NewsAddress.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import jakarta.mail.Address; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.StringTokenizer; + +/** + * This class models an RFC1036 newsgroup address. + * + * @author Bill Shannon + * @author John Mani + */ +@SuppressWarnings("serial") +public class NewsAddress extends Address { + + /** + * The newsgroup. + */ + protected String newsgroup; + + /** + * The host. May be {@code null}. + */ + protected String host; + + /** + * Default constructor. + */ + public NewsAddress() { + } + + /** + * Construct a NewsAddress with the given newsgroup. + * + * @param newsgroup the newsgroup + */ + public NewsAddress(String newsgroup) { + this(newsgroup, null); + } + + /** + * Construct a NewsAddress with the given newsgroup and host. + * + * @param newsgroup the newsgroup + * @param host the host + */ + public NewsAddress(String newsgroup, String host) { + // XXX - this method should throw an exception so we can report + // illegal addresses, but for now just remove whitespace + this.newsgroup = newsgroup.replaceAll("\\s+", ""); + this.host = host; + } + + /** + * Convert the given array of NewsAddress objects into + * a comma separated sequence of address strings. The + * resulting string contains only US-ASCII characters, and + * hence is mail-safe. + * + * @param addresses array of NewsAddress objects + * @return comma separated address strings + * @throws ClassCastException if any address object in the + * given array is not a NewsAddress objects. Note + * that this is a RuntimeException. + */ + public static String toString(Address[] addresses) { + if (addresses == null || addresses.length == 0) + return null; + + StringBuilder s = + new StringBuilder(addresses[0].toString()); + int used = s.length(); + for (int i = 1; i < addresses.length; i++) { + s.append(","); + used++; + String ng = addresses[i].toString(); + if (used + ng.length() > 76) { + s.append("\r\n\t"); + used = 8; + } + s.append(ng); + used += ng.length(); + } + + return s.toString(); + } + + /** + * Parse the given comma separated sequence of newsgroups into + * NewsAddress objects. + * + * @param newsgroups comma separated newsgroup string + * @return array of NewsAddress objects + * @throws AddressException if the parse failed + */ + public static NewsAddress[] parse(String newsgroups) + throws AddressException { + // XXX - verify format of newsgroup name? + StringTokenizer st = new StringTokenizer(newsgroups, ","); + List nglist = new ArrayList<>(); + while (st.hasMoreTokens()) { + String ng = st.nextToken(); + nglist.add(new NewsAddress(ng)); + } + return nglist.toArray(new NewsAddress[0]); + } + + /** + * Return the type of this address. The type of a NewsAddress + * is "news". + */ + @Override + public String getType() { + return "news"; + } + + /** + * Get the newsgroup. + * + * @return newsgroup + */ + public String getNewsgroup() { + return newsgroup; + } + + /** + * Set the newsgroup. + * + * @param newsgroup the newsgroup + */ + public void setNewsgroup(String newsgroup) { + this.newsgroup = newsgroup; + } + + /** + * Get the host. + * + * @return host + */ + public String getHost() { + return host; + } + + /** + * Set the host. + * + * @param host the host + */ + public void setHost(String host) { + this.host = host; + } + + /** + * Convert this address into a RFC 1036 address. + * + * @return newsgroup + */ + @Override + public String toString() { + return newsgroup; + } + + /** + * The equality operator. + */ + @Override + public boolean equals(Object a) { + if (!(a instanceof NewsAddress)) + return false; + + NewsAddress s = (NewsAddress) a; + return ((newsgroup == null && s.newsgroup == null) || + (newsgroup != null && newsgroup.equals(s.newsgroup))) && + ((host == null && s.host == null) || + (host != null && s.host != null && host.equalsIgnoreCase(s.host))); + } + + /** + * Compute a hash code for the address. + */ + @Override + public int hashCode() { + int hash = 0; + if (newsgroup != null) + hash += newsgroup.hashCode(); + if (host != null) + hash += host.toLowerCase(Locale.ENGLISH).hashCode(); + return hash; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/ParameterList.java b/net-mail/src/main/java/jakarta/mail/internet/ParameterList.java new file mode 100644 index 0000000..ea92117 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/ParameterList.java @@ -0,0 +1,886 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.ArrayList; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * This class holds MIME parameters (attribute-value pairs). + * The mail.mime.encodeparameters and + * mail.mime.decodeparameters System properties + * control whether encoded parameters, as specified by + * RFC 2231, + * are supported. By default, such encoded parameters are + * supported.

+ *

+ * Also, in the current implementation, setting the System property + * mail.mime.decodeparameters.strict to "true" + * will cause a ParseException to be thrown for errors + * detected while decoding encoded parameters. By default, if any + * decoding errors occur, the original (undecoded) string is used.

+ *

+ * The current implementation supports the System property + * mail.mime.parameters.strict, which if set to false + * when parsing a parameter list allows parameter values + * to contain whitespace and other special characters without + * being quoted; the parameter value ends at the next semicolon. + * If set to true (the default), parameter values are required to conform + * to the MIME specification and must be quoted if they contain whitespace + * or special characters. + * + * @author John Mani + * @author Bill Shannon + */ + +public class ParameterList { + + private static final boolean encodeParameters = + MimeUtility.getBooleanSystemProperty("mail.mime.encodeparameters", true); + private static final boolean decodeParameters = + MimeUtility.getBooleanSystemProperty("mail.mime.decodeparameters", true); + private static final boolean decodeParametersStrict = + MimeUtility.getBooleanSystemProperty( + "mail.mime.decodeparameters.strict", false); + private static final boolean applehack = + MimeUtility.getBooleanSystemProperty("mail.mime.applefilenames", false); + private static final boolean windowshack = + MimeUtility.getBooleanSystemProperty("mail.mime.windowsfilenames", false); + private static final boolean parametersStrict = + MimeUtility.getBooleanSystemProperty("mail.mime.parameters.strict", true); + private static final boolean splitLongParameters = + MimeUtility.getBooleanSystemProperty( + "mail.mime.splitlongparameters", true); + private static final char[] hex = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + /** + * The map of name, value pairs. + * The value object is either a String, for unencoded + * values, or a Value object, for encoded values, + * or a MultiValue object, for multi-segment parameters, + * or a LiteralValue object for strings that should not be encoded. + *

+ * We use a LinkedHashMap so that parameters are (as much as + * possible) kept in the original order. Note however that + * multi-segment parameters (see below) will appear in the + * position of the first seen segment and orphan segments + * will all move to the end. + */ + // keep parameters in order + private Map list = new LinkedHashMap<>(); + /** + * A set of names for multi-segment parameters that we + * haven't processed yet. Normally such names are accumulated + * during the inital parse and processed at the end of the parse, + * but such names can also be set via the set method when the + * IMAP provider accumulates pre-parsed pieces of a parameter list. + * (A special call to the set method tells us when the IMAP provider + * is done setting parameters.) + *

+ * A multi-segment parameter is defined by RFC 2231. For example, + * "title*0=part1; title*1=part2", which represents a parameter + * named "title" with value "part1part2". + *

+ * Note also that each segment of the value might or might not be + * encoded, indicated by a trailing "*" on the parameter name. + * If any segment is encoded, the first segment must be encoded. + * Only the first segment contains the charset and language + * information needed to decode any encoded segments. + *

+ * RFC 2231 introduces many possible failure modes, which we try + * to handle as gracefully as possible. Generally, a failure to + * decode a parameter value causes the non-decoded parameter value + * to be used instead. Missing segments cause all later segments + * to be appear as independent parameters with names that include + * the segment number. For example, "title*0=part1; title*1=part2; + * title*3=part4" appears as two parameters named "title" and "title*3". + */ + private Set multisegmentNames; + /** + * A map containing the segments for all not-yet-processed + * multi-segment parameters. The map is indexed by "name*seg". + * The value object is either a String or a Value object. + * The Value object is not decoded during the initial parse + * because the segments may appear in any order and until the + * first segment appears we don't know what charset to use to + * decode the encoded segments. The segments are hex decoded + * in order, combined into a single byte array, and converted + * to a String using the specified charset in the + * combineMultisegmentNames method. + */ + private Map slist; + /** + * MWB 3BView: The name of the last parameter added to the map. + * Used for the AppleMail hack. + */ + private String lastName = null; + + /** + * No-arg Constructor. + */ + public ParameterList() { + // initialize other collections only if they'll be needed + if (decodeParameters) { + multisegmentNames = new HashSet<>(); + slist = new HashMap<>(); + } + } + + /** + * Constructor that takes a parameter-list string. The String + * is parsed and the parameters are collected and stored internally. + * A ParseException is thrown if the parse fails. + * Note that an empty parameter-list string is valid and will be + * parsed into an empty ParameterList. + * + * @param s the parameter-list string. + * @throws ParseException if the parse fails. + */ + public ParameterList(String s) throws ParseException { + this(); + + HeaderTokenizer h = new HeaderTokenizer(s, HeaderTokenizer.MIME); + for (; ; ) { + HeaderTokenizer.Token tk = h.next(); + int type = tk.getType(); + String name, value; + + if (type == HeaderTokenizer.Token.EOF) // done + break; + + if ((char) type == ';') { + // expect parameter name + tk = h.next(); + // tolerate trailing semicolon, even though it violates the spec + if (tk.getType() == HeaderTokenizer.Token.EOF) + break; + // parameter name must be a MIME Atom + if (tk.getType() != HeaderTokenizer.Token.ATOM) + throw new ParseException("In parameter list <" + s + ">" + + ", expected parameter name, " + + "got \"" + tk.getValue() + "\""); + name = tk.getValue().toLowerCase(Locale.ENGLISH); + + // expect '=' + tk = h.next(); + if ((char) tk.getType() != '=') + throw new ParseException("In parameter list <" + s + ">" + + ", expected '=', " + + "got \"" + tk.getValue() + "\""); + + // expect parameter value + if (windowshack && + (name.equals("name") || name.equals("filename"))) + tk = h.next(';', true); + else if (parametersStrict) + tk = h.next(); + else + tk = h.next(';'); + type = tk.getType(); + // parameter value must be a MIME Atom or Quoted String + if (type != HeaderTokenizer.Token.ATOM && + type != HeaderTokenizer.Token.QUOTEDSTRING) + throw new ParseException("In parameter list <" + s + ">" + + ", expected parameter value, " + + "got \"" + tk.getValue() + "\""); + + value = tk.getValue(); + lastName = name; + if (decodeParameters) + putEncodedName(name, value); + else + list.put(name, value); + } else { + // MWB 3BView new code to add in filenames generated by + // AppleMail. + // Note - one space is assumed between name elements. + // This may not be correct but it shouldn't matter too much. + // Note: AppleMail encodes filenames with non-ascii characters + // correctly, so we don't need to worry about the name* subkeys. + if (type == HeaderTokenizer.Token.ATOM && lastName != null && + ((applehack && + (lastName.equals("name") || + lastName.equals("filename"))) || + !parametersStrict) + ) { + // Add value to previous value + String lastValue = (String) list.get(lastName); + value = lastValue + " " + tk.getValue(); + list.put(lastName, value); + } else { + throw new ParseException("In parameter list <" + s + ">" + + ", expected ';', got \"" + + tk.getValue() + "\""); + } + } + } + + if (decodeParameters) { + /* + * After parsing all the parameters, combine all the + * multi-segment parameter values together. + */ + combineMultisegmentNames(false); + } + } + + // Quote a parameter value token if required. + private static String quote(String value) { + return MimeUtility.quote(value, HeaderTokenizer.MIME); + } + + /** + * Encode a parameter value, if necessary. + * If the value is encoded, a Value object is returned. + * Otherwise, null is returned. + * XXX - Could return a MultiValue object if parameter value is too long. + */ + private static Value encodeValue(String value, String charset) { + if (MimeUtility.checkAscii(value) == MimeUtility.ALL_ASCII) + return null; // no need to encode it + + byte[] b; // charset encoded bytes from the string + try { + b = value.getBytes(MimeUtility.javaCharset(charset)); + } catch (UnsupportedEncodingException ex) { + return null; + } + StringBuilder sb = new StringBuilder(b.length + charset.length() + 2); + sb.append(charset).append("''"); + for (int i = 0; i < b.length; i++) { + char c = (char) (b[i] & 0xff); + // do we need to encode this character? + if (c <= ' ' || c >= 0x7f || c == '*' || c == '\'' || c == '%' || + HeaderTokenizer.MIME.indexOf(c) >= 0) { + sb.append('%').append(hex[c >> 4]).append(hex[c & 0xf]); + } else + sb.append(c); + } + Value v = new Value(); + v.charset = charset; + v.value = value; + v.encodedValue = sb.toString(); + return v; + } + + /** + * Extract charset and encoded value. + * Value will be decoded later. + */ + private static Value extractCharset(String value) throws ParseException { + Value v = new Value(); + v.value = v.encodedValue = value; + try { + int i = value.indexOf('\''); + if (i < 0) { + if (decodeParametersStrict) + throw new ParseException( + "Missing charset in encoded value: " + value); + return v; // not encoded correctly? return as is. + } + String charset = value.substring(0, i); + int li = value.indexOf('\'', i + 1); + if (li < 0) { + if (decodeParametersStrict) + throw new ParseException( + "Missing language in encoded value: " + value); + return v; // not encoded correctly? return as is. + } + // String lang = value.substring(i + 1, li); + v.value = value.substring(li + 1); + v.charset = charset; + } catch (NumberFormatException | StringIndexOutOfBoundsException nex) { + if (decodeParametersStrict) + throw new ParseException(nex.toString()); + } + return v; + } + + /** + * Decode the encoded bytes in value using the specified charset. + */ + private static String decodeBytes(String value, String charset) + throws ParseException, UnsupportedEncodingException { + /* + * Decode the ASCII characters in value + * into an array of bytes, and then convert + * the bytes to a String using the specified + * charset. We'll never need more bytes than + * encoded characters, so use that to size the + * array. + */ + byte[] b = new byte[value.length()]; + int i, bi; + for (i = 0, bi = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '%') { + try { + String hex = value.substring(i + 1, i + 3); + c = (char) Integer.parseInt(hex, 16); + i += 2; + } catch (NumberFormatException | StringIndexOutOfBoundsException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + } + b[bi++] = (byte) c; + } + if (charset != null) + charset = MimeUtility.javaCharset(charset); + if (charset == null || charset.length() == 0) + charset = MimeUtility.getDefaultJavaCharset(); + return new String(b, 0, bi, charset); + } + + /** + * Decode the encoded bytes in value and write them to the OutputStream. + */ + private static void decodeBytes(String value, OutputStream os) + throws ParseException, IOException { + /* + * Decode the ASCII characters in value + * and write them to the stream. + */ + int i; + for (i = 0; i < value.length(); i++) { + char c = value.charAt(i); + if (c == '%') { + try { + String hex = value.substring(i + 1, i + 3); + c = (char) Integer.parseInt(hex, 16); + i += 2; + } catch (NumberFormatException | StringIndexOutOfBoundsException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + } + os.write((byte) c); + } + } + + /** + * Normal users of this class will use simple parameter names. + * In some cases, for example, when processing IMAP protocol + * messages, individual segments of a multi-segment name + * (specified by RFC 2231) will be encountered and passed to + * the {@link #set} method. After all these segments are added + * to this ParameterList, they need to be combined to represent + * the logical parameter name and value. This method will combine + * all segments of multi-segment names.

+ *

+ * Normal users should never need to call this method. + * + * @since JavaMail 1.5 + */ + public void combineSegments() { + /* + * If we've accumulated any multi-segment names from calls to + * the set method from (e.g.) the IMAP provider, combine the pieces. + * Ignore any parse errors (e.g., from decoding the values) + * because it's too late to report them. + */ + if (decodeParameters && multisegmentNames.size() > 0) { + try { + combineMultisegmentNames(true); + } catch (ParseException pex) { + // too late to do anything about it + } + } + } + + /** + * If the name is an encoded or multi-segment name (or both) + * handle it appropriately, storing the appropriate String + * or Value object. Multi-segment names are stored in the + * main parameter list as an emtpy string as a placeholder, + * replaced later in combineMultisegmentNames with a MultiValue + * object. This causes all pieces of the multi-segment parameter + * to appear in the position of the first seen segment of the + * parameter. + */ + private void putEncodedName(String name, String value) + throws ParseException { + int star = name.indexOf('*'); + if (star < 0) { + // single parameter, unencoded value + list.put(name, value); + } else if (star == name.length() - 1) { + // single parameter, encoded value + name = name.substring(0, star); + Value v = extractCharset(value); + try { + v.value = decodeBytes(v.value, v.charset); + } catch (UnsupportedEncodingException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + list.put(name, v); + } else { + // multiple segments + String rname = name.substring(0, star); + multisegmentNames.add(rname); + list.put(rname, ""); + + Object v; + if (name.endsWith("*")) { + // encoded value + if (name.endsWith("*0*")) { // first segment + v = extractCharset(value); + } else { + v = new Value(); + ((Value) v).encodedValue = value; + ((Value) v).value = value; // default; decoded later + } + name = name.substring(0, name.length() - 1); + } else { + // unencoded value + v = value; + } + slist.put(name, v); + } + } + + /** + * Iterate through the saved set of names of multi-segment parameters, + * for each parameter find all segments stored in the slist map, + * decode each segment as needed, combine the segments together into + * a single decoded value, and save all segments in a MultiValue object + * in the main list indexed by the parameter name. + */ + private void combineMultisegmentNames(boolean keepConsistentOnFailure) + throws ParseException { + boolean success = false; + try { + Iterator it = multisegmentNames.iterator(); + while (it.hasNext()) { + String name = it.next(); + MultiValue mv = new MultiValue(); + /* + * Now find all the segments for this name and + * decode each segment as needed. + */ + String charset = null; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + int segment; + for (segment = 0; ; segment++) { + String sname = name + "*" + segment; + Object v = slist.get(sname); + if (v == null) // out of segments + break; + mv.add(v); + try { + if (v instanceof Value) { + Value vv = (Value) v; + if (segment == 0) { + // the first segment specifies the charset + // for all other encoded segments + charset = vv.charset; + } else { + if (charset == null) { + // should never happen + multisegmentNames.remove(name); + break; + } + } + decodeBytes(vv.value, bos); + } else { + bos.write(MimeUtility.getBytes((String) v)); + } + } catch (IOException ex) { + // XXX - should never happen + } + slist.remove(sname); + } + if (segment == 0) { + // didn't find any segments at all + list.remove(name); + } else { + try { + if (charset != null) + charset = MimeUtility.javaCharset(charset); + if (charset == null || charset.length() == 0) + charset = MimeUtility.getDefaultJavaCharset(); + if (charset != null) + mv.value = bos.toString(charset); + else + mv.value = bos.toString(); + } catch (UnsupportedEncodingException uex) { + if (decodeParametersStrict) + throw new ParseException(uex.toString()); + // convert as if iso-8859-1 + try { + mv.value = bos.toString("iso-8859-1"); + } catch (UnsupportedEncodingException ex) { + // should never happen + } + } + list.put(name, mv); + } + } + success = true; + } finally { + /* + * If we get here because of an exception that's going to + * be thrown (success == false) from the constructor + * (keepConsistentOnFailure == false), this is all wasted effort. + */ + if (keepConsistentOnFailure || success) { + // we should never end up with anything in slist, + // but if we do, add it all to list + if (slist.size() > 0) { + // first, decode any values that we'll add to the list + Iterator sit = slist.values().iterator(); + while (sit.hasNext()) { + Object v = sit.next(); + if (v instanceof Value) { + Value vv = (Value) v; + try { + vv.value = + decodeBytes(vv.value, vv.charset); + } catch (UnsupportedEncodingException ex) { + if (decodeParametersStrict) + throw new ParseException(ex.toString()); + } + } + } + list.putAll(slist); + } + + // clear out the set of names and segments + multisegmentNames.clear(); + slist.clear(); + } + } + } + + /** + * Return the number of parameters in this list. + * + * @return number of parameters. + */ + public int size() { + return list.size(); + } + + /** + * Returns the value of the specified parameter. Note that + * parameter names are case-insensitive. + * + * @param name parameter name. + * @return Value of the parameter. Returns + * null if the parameter is not + * present. + */ + public String get(String name) { + String value; + Object v = list.get(name.trim().toLowerCase(Locale.ENGLISH)); + if (v instanceof MultiValue) + value = ((MultiValue) v).value; + else if (v instanceof LiteralValue) + value = ((LiteralValue) v).value; + else if (v instanceof Value) + value = ((Value) v).value; + else + value = (String) v; + return value; + } + + /** + * Set a parameter. If this parameter already exists, it is + * replaced by this new value. + * + * @param name name of the parameter. + * @param value value of the parameter. + */ + public void set(String name, String value) { + name = name.trim().toLowerCase(Locale.ENGLISH); + if (decodeParameters) { + try { + putEncodedName(name, value); + } catch (ParseException pex) { + // ignore it + list.put(name, value); + } + } else + list.put(name, value); + } + + /** + * Set a parameter. If this parameter already exists, it is + * replaced by this new value. If the + * mail.mime.encodeparameters System property + * is true, and the parameter value is non-ASCII, it will be + * encoded with the specified charset, as specified by RFC 2231. + * + * @param name name of the parameter. + * @param value value of the parameter. + * @param charset charset of the parameter value. + * @since JavaMail 1.4 + */ + public void set(String name, String value, String charset) { + if (encodeParameters) { + Value ev = encodeValue(value, charset); + // was it actually encoded? + if (ev != null) + list.put(name.trim().toLowerCase(Locale.ENGLISH), ev); + else + set(name, value); + } else + set(name, value); + } + + /** + * Package-private method to set a literal value that won't be + * further encoded. Used to set the filename parameter when + * "mail.mime.encodefilename" is true. + * + * @param name name of the parameter. + * @param value value of the parameter. + */ + void setLiteral(String name, String value) { + LiteralValue lv = new LiteralValue(); + lv.value = value; + list.put(name, lv); + } + + /** + * Removes the specified parameter from this ParameterList. + * This method does nothing if the parameter is not present. + * + * @param name name of the parameter. + */ + public void remove(String name) { + list.remove(name.trim().toLowerCase(Locale.ENGLISH)); + } + + /** + * Return an enumeration of the names of all parameters in this + * list. + * + * @return Enumeration of all parameter names in this list. + */ + public Enumeration getNames() { + return new ParamEnum(list.keySet().iterator()); + } + + /** + * Convert this ParameterList into a MIME String. If this is + * an empty list, an empty string is returned. + * + * @return String + */ + @Override + public String toString() { + return toString(0); + } + + /** + * Convert this ParameterList into a MIME String. If this is + * an empty list, an empty string is returned. + *

+ * The 'used' parameter specifies the number of character positions + * already taken up in the field into which the resulting parameter + * list is to be inserted. It's used to determine where to fold the + * resulting parameter list. + * + * @param used number of character positions already used, in + * the field into which the parameter list is to + * be inserted. + * @return String + */ + public String toString(int used) { + ToStringBuffer sb = new ToStringBuffer(used); + Iterator> e = list.entrySet().iterator(); + + while (e.hasNext()) { + Map.Entry ent = e.next(); + String name = ent.getKey(); + String value; + Object v = ent.getValue(); + if (v instanceof MultiValue) { + MultiValue vv = (MultiValue) v; + name += "*"; + for (int i = 0; i < vv.size(); i++) { + Object va = vv.get(i); + String ns; + if (va instanceof Value) { + ns = name + i + "*"; + value = ((Value) va).encodedValue; + } else { + ns = name + i; + value = (String) va; + } + sb.addNV(ns, quote(value)); + } + } else if (v instanceof LiteralValue) { + value = ((LiteralValue) v).value; + sb.addNV(name, quote(value)); + } else if (v instanceof Value) { + /* + * XXX - We could split the encoded value into multiple + * segments if it's too long, but that's more difficult. + */ + name += "*"; + value = ((Value) v).encodedValue; + sb.addNV(name, quote(value)); + } else { + value = (String) v; + /* + * If this value is "long", split it into a multi-segment + * parameter. Only do this if we've enabled RFC2231 style + * encoded parameters. + * + * Note that we check the length before quoting the value. + * Quoting might make the string longer, although typically + * not much, so we allow a little slop in the calculation. + * In the worst case, a 60 character string will turn into + * 122 characters when quoted, which is long but not + * outrageous. + */ + if (value.length() > 60 && + splitLongParameters && encodeParameters) { + int seg = 0; + name += "*"; + while (value.length() > 60) { + sb.addNV(name + seg, quote(value.substring(0, 60))); + value = value.substring(60); + seg++; + } + if (value.length() > 0) + sb.addNV(name + seg, quote(value)); + } else { + sb.addNV(name, quote(value)); + } + } + } + return sb.toString(); + } + + /** + * A struct to hold an encoded value. + * A parsed encoded value is stored as both the + * decoded value and the original encoded value + * (so that toString will produce the same result). + * An encoded value that is set explicitly is stored + * as the original value and the encoded value, to + * ensure that get will return the same value that + * was set. + */ + private static class Value { + String value; + String charset; + String encodedValue; + } + + /** + * A struct to hold a literal value that shouldn't be further encoded. + */ + private static class LiteralValue { + String value; + } + + /** + * A struct for a multi-segment parameter. Each entry in the + * List is either a String or a Value object. When all the + * segments are present and combined in the combineMultisegmentNames + * method, the value field contains the combined and decoded value. + * Until then the value field contains an empty string as a placeholder. + */ + @SuppressWarnings("serial") + private static class MultiValue extends ArrayList { + + String value; + } + + /** + * Map the LinkedHashMap's keySet iterator to an Enumeration. + */ + private static class ParamEnum implements Enumeration { + private Iterator it; + + ParamEnum(Iterator it) { + this.it = it; + } + + @Override + public boolean hasMoreElements() { + return it.hasNext(); + } + + @Override + public String nextElement() { + return it.next(); + } + } + + /** + * A special wrapper for a StringBuffer that keeps track of the + * number of characters used in a line, wrapping to a new line + * as necessary; for use by the toString method. + */ + private static class ToStringBuffer { + private int used; // keep track of how much used on current line + private StringBuilder sb = new StringBuilder(); + + public ToStringBuffer(int used) { + this.used = used; + } + + public void addNV(String name, String value) { + sb.append("; "); + used += 2; + int len = name.length() + value.length() + 1; + if (used + len > 76) { // overflows ... + sb.append("\r\n\t"); // .. start new continuation line + used = 8; // account for the starting char + } + sb.append(name).append('='); + used += name.length() + 1; + if (used + value.length() > 76) { // still overflows ... + // have to fold value + String s = MimeUtility.fold(used, value); + sb.append(s); + int lastlf = s.lastIndexOf('\n'); + if (lastlf >= 0) // always true + used += s.length() - lastlf - 1; + else + used += s.length(); + } else { + sb.append(value); + used += value.length(); + } + } + + @Override + public String toString() { + return sb.toString(); + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/ParseException.java b/net-mail/src/main/java/jakarta/mail/internet/ParseException.java new file mode 100644 index 0000000..b7b2f0c --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/ParseException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import jakarta.mail.MessagingException; + +/** + * The exception thrown due to an error in parsing RFC822 + * or MIME headers, including multipart bodies. + * + * @author John Mani + */ +@SuppressWarnings("serial") +public class ParseException extends MessagingException { + + /** + * Constructs a ParseException with no detail message. + */ + public ParseException() { + super(); + } + + /** + * Constructs a ParseException with the specified detail message. + * + * @param s the detail message + */ + public ParseException(String s) { + super(s); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/PreencodedMimeBodyPart.java b/net-mail/src/main/java/jakarta/mail/internet/PreencodedMimeBodyPart.java new file mode 100644 index 0000000..e77fc28 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/PreencodedMimeBodyPart.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import jakarta.mail.MessagingException; +import jakarta.mail.util.LineOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Enumeration; + +/** + * A MimeBodyPart that handles data that has already been encoded. + * This class is useful when constructing a message and attaching + * data that has already been encoded (for example, using base64 + * encoding). The data may have been encoded by the application, + * or may have been stored in a file or database in encoded form. + * The encoding is supplied when this object is created. The data + * is attached to this object in the usual fashion, by using the + * setText, setContent, or + * setDataHandler methods. + * + * @since JavaMail 1.4 + */ + +public class PreencodedMimeBodyPart extends MimeBodyPart { + private String encoding; + + /** + * Create a PreencodedMimeBodyPart that assumes the data is + * encoded using the specified encoding. The encoding must + * be a MIME supported Content-Transfer-Encoding. + * + * @param encoding the Content-Transfer-Encoding + */ + public PreencodedMimeBodyPart(String encoding) { + this.encoding = encoding; + } + + /** + * Returns the content transfer encoding specified when + * this object was created. + */ + @Override + public String getEncoding() throws MessagingException { + return encoding; + } + + /** + * Output the body part as an RFC 822 format stream. + * + * @throws IOException if an error occurs writing to the + * stream or if an error is generated + * by the jakarta.activation layer. + * @throws MessagingException for other failures + * @see jakarta.activation.DataHandler#writeTo + */ + @Override + public void writeTo(OutputStream os) + throws IOException, MessagingException { + + // see if we already have a LOS + LineOutputStream los = null; + if (os instanceof LineOutputStream) { + los = (LineOutputStream) os; + } else { + los = streamProvider.outputLineStream(os, false); + } + + // First, write out the header + Enumeration hdrLines = getAllHeaderLines(); + while (hdrLines.hasMoreElements()) + los.writeln(hdrLines.nextElement()); + + // The CRLF separator between header and content + los.writeln(); + + // Finally, the content, already encoded. + getDataHandler().writeTo(os); + os.flush(); + } + + /** + * Force the Content-Transfer-Encoding header to use + * the encoding that was specified when this object was created. + */ + @Override + protected void updateHeaders() throws MessagingException { + super.updateHeaders(); + MimeBodyPart.setEncoding(this, encoding); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/SharedInputStream.java b/net-mail/src/main/java/jakarta/mail/internet/SharedInputStream.java new file mode 100644 index 0000000..396c49b --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/SharedInputStream.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import java.io.InputStream; + +/** + * An InputStream that is backed by data that can be shared by multiple + * readers may implement this interface. This allows users of such an + * InputStream to determine the current position in the InputStream, and + * to create new InputStreams representing a subset of the data in the + * original InputStream. The new InputStream will access the same + * underlying data as the original, without copying the data.

+ *

+ * Note that implementations of this interface must ensure that the + * close method does not close any underlying stream + * that might be shared by multiple instances of SharedInputStream + * until all shared instances have been closed. + * + * @author Bill Shannon + * @since JavaMail 1.2 + */ + +public interface SharedInputStream { + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + long getPosition(); + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at start (inclusive) + * up to end (exclusive). start must be + * non-negative. If end is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + InputStream newStream(long start, long end); +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/UniqueValue.java b/net-mail/src/main/java/jakarta/mail/internet/UniqueValue.java new file mode 100644 index 0000000..35572b5 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/UniqueValue.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.internet; + +import jakarta.mail.Session; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * This is a utility class that generates unique values. The generated + * String contains only US-ASCII characters and hence is safe for use + * in RFC822 headers.

+ *

+ * This is a package private class. + * + * @author John Mani + * @author Max Spivak + * @author Bill Shannon + */ + +class UniqueValue { + /** + * A global unique number, to ensure uniqueness of generated strings. + */ + private static AtomicInteger id = new AtomicInteger(); + + /** + * Get a unique value for use in a multipart boundary string. + *

+ * This implementation generates it by concatenating a global + * part number, a newly created object's hashCode(), + * and the current time (in milliseconds). + */ + public static String getUniqueBoundaryValue() { + StringBuilder s = new StringBuilder(); + long hash = s.hashCode(); + + // Unique string is ----=_Part__. + s.append("----=_Part_").append(id.getAndIncrement()).append("_"). + append(hash).append('.'). + append(System.currentTimeMillis()); + return s.toString(); + } + + /** + * Get a unique value for use in a Message-ID. + *

+ * This implementation generates it by concatenating a newly + * created object's hashCode(), a global ID + * (incremented on every use), the current time (in milliseconds), + * and the host name from this user's local address generated by + * InternetAddress.getLocalAddress(). + * (The host name defaults to "localhost" if + * getLocalAddress() returns null.) + * + * @param ssn Session object used to get the local address + * @see InternetAddress + */ + public static String getUniqueMessageIDValue(Session ssn) { + String suffix = null; + + InternetAddress addr = InternetAddress.getLocalAddress(ssn); + if (addr != null) + suffix = addr.getAddress(); + else { + suffix = "jakartamailuser@localhost"; // worst-case default + } + int at = suffix.lastIndexOf('@'); + if (at >= 0) + suffix = suffix.substring(at); + + StringBuilder s = new StringBuilder(); + + // Unique string is .. + s.append(s.hashCode()).append('.'). + append(id.getAndIncrement()).append('.'). + append(System.currentTimeMillis()). + append(suffix); + return s.toString(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/internet/package-info.java b/net-mail/src/main/java/jakarta/mail/internet/package-info.java new file mode 100644 index 0000000..534e1a2 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/internet/package-info.java @@ -0,0 +1,542 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * Classes specific to Internet mail systems. + * This package supports features that are specific to Internet mail systems + * based on the MIME standard + * (RFC 2045, + * RFC 2046, and + * RFC 2047). + * The IMAP, SMTP, and POP3 protocols use + * {@link jakarta.mail.internet.MimeMessage MimeMessages}. + * + * Properties + *

+ * The Jakarta Mail API supports the following standard properties, + * which may be set in the Session object, or in the + * Properties object used to create the Session object. + * The properties are always set as strings; the Type column describes + * how the string is interpreted. For example, use + *

+ *
+ * session.setProperty("mail.mime.address.strict", "false");
+ * 
+ *

+ * to set the mail.mime.address.strict property, + * which is of type boolean. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Jakarta Mail properties
NameTypeDescription
mail.mime.address.strictboolean + * The mail.mime.address.strict session property controls + * the parsing of address headers. By default, strict parsing of address + * headers is done. If this property is set to "false", + * strict parsing is not done and many illegal addresses that sometimes + * occur in real messages are allowed. See the InternetAddress + * class for details. + *
mail.mime.allowutf8boolean + * If set to "true", UTF-8 strings are allowed in message headers, + * e.g., in addresses. This should only be set if the mail server also + * supports UTF-8. + *
+ *

+ * The Jakarta Mail API specification requires support for the following properties, + * which must be set in the System properties. + * The properties are always set as strings; the Type column describes + * how the string is interpreted. For example, use + *

+ *
+ * System.setProperty("mail.mime.decodetext.strict", "false");
+ * 
+ *

+ * to set the mail.mime.decodetext.strict property, + * which is of type boolean. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Jakarta Mail System properties
NameTypeDescription
mail.mime.charsetString + * The mail.mime.charset System property can + * be used to specify the default MIME charset to use for encoded words + * and text parts that don't otherwise specify a charset. Normally, the + * default MIME charset is derived from the default Java charset, as + * specified in the file.encoding System property. Most + * applications will have no need to explicitly set the default MIME + * charset. In cases where the default MIME charset to be used for + * mail messages is different than the charset used for files stored on + * the system, this property should be set. + *
mail.mime.decodetext.strictboolean + * The mail.mime.decodetext.strict property controls + * decoding of MIME encoded words. The MIME spec requires that encoded + * words start at the beginning of a whitespace separated word. Some + * mailers incorrectly include encoded words in the middle of a word. + * If the mail.mime.decodetext.strict System property is + * set to "false", an attempt will be made to decode these + * illegal encoded words. The default is true. + *
mail.mime.encodeeol.strictboolean + * The mail.mime.encodeeol.strict property controls the + * choice of Content-Transfer-Encoding for MIME parts that are not of + * type "text". Often such parts will contain textual data for which + * an encoding that allows normal end of line conventions is appropriate. + * In rare cases, such a part will appear to contain entirely textual + * data, but will require an encoding that preserves CR and LF characters + * without change. If the mail.mime.encodeeol.strict + * System property is set to "true", such an encoding will + * be used when necessary. The default is false. + *
mail.mime.decodefilenameboolean + * If set to "true", the getFileName method + * uses the MimeUtility + * method decodeText to decode any + * non-ASCII characters in the filename. Note that this decoding + * violates the MIME specification, but is useful for interoperating + * with some mail clients that use this convention. + * The default is false. + *
mail.mime.encodefilenameboolean + * If set to "true", the setFileName method + * uses the MimeUtility + * method encodeText to encode any + * non-ASCII characters in the filename. Note that this encoding + * violates the MIME specification, but is useful for interoperating + * with some mail clients that use this convention. + * The default is false. + *
mail.mime.decodeparametersboolean + * If set to "false", non-ASCII parameters in a + * ParameterList, e.g., in a Content-Type header, + * will not be decoded as specified by + * RFC 2231. + * The default is true. + *
mail.mime.encodeparametersboolean + * If set to "false", non-ASCII parameters in a + * ParameterList, e.g., in a Content-Type header, + * will not be encoded as specified by + * RFC 2231. + * The default is true. + *
mail.mime.multipart. ignoremissingendboundaryboolean + * Normally, when parsing a multipart MIME message, a message that is + * missing the final end boundary line is not considered an error. + * The data simply ends at the end of the input. Note that messages + * of this form violate the MIME specification. If the property + * mail.mime.multipart.ignoremissingendboundary is set + * to false, such messages are considered an error and a + * MesagingException will be thrown when parsing such a + * message. + *
mail.mime.multipart. ignoremissingboundaryparameterboolean + * If the Content-Type header for a multipart content does not have + * a boundary parameter, the multipart parsing code + * will look for the first line in the content that looks like a + * boundary line and extract the boundary parameter from the line. + * If this property is set to "false", a + * MessagingException will be thrown if the Content-Type + * header doesn't specify a boundary parameter. + * The default is true. + *
mail.mime.multipart. ignoreexistingboundaryparameterboolean + * Normally the boundary parameter in the Content-Type header of a multipart + * body part is used to specify the separator between parts of the multipart + * body. This System property may be set to "true" to cause + * the parser to look for a line in the multipart body that looks like a + * boundary line and use that value as the separator between subsequent parts. + * This may be useful in cases where a broken anti-virus product has rewritten + * the message incorrectly such that the boundary parameter and the actual + * boundary value no longer match. + * The default value of this property is false. + *
mail.mime.multipart. allowemptyboolean + * Normally, when writing out a MimeMultipart that contains no body + * parts, or when trying to parse a multipart message with no body parts, + * a MessagingException is thrown. The MIME spec does not allow + * multipart content with no body parts. This + * System property may be set to "true" to override this behavior. + * When writing out such a MimeMultipart, a single empty part will be + * included. When reading such a multipart, a MimeMultipart will be created + * with no body parts. + * The default value of this property is false. + *
+ * + * + *

+ * The following properties are supported by the EE4J implementation of + * Jakarta Mail, but are not currently a required part of the specification. + * These must be set as Session properties. + * The names, types, defaults, and semantics of these properties may + * change in future releases. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Jakarta Mail implementation properties
NameTypeDescription
mail.alternatesString + * A string containing other email addresses that the current user is known by. + * The MimeMessage reply method will eliminate any + * of these addresses from the recipient list in the message it constructs, + * to avoid sending the reply back to the sender. + *
mail.replyallccboolean + * If set to "true", the MimeMessage + * reply method will put all recipients except the original + * sender in the Cc list of the newly constructed message. + * Normally, recipients in the To header of the original + * message will also appear in the To list of the newly + * constructed message. + *
+ * + *

+ * The following properties are supported by the EE4J implementation of + * Jakarta Mail, but are not currently a required part of the specification. + * These must be set as System properties. + * The names, types, defaults, and semantics of these properties may + * change in future releases. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Jakarta Mail implementation System properties
NameTypeDescription
mail.mime.base64.ignoreerrorsboolean + * If set to "true", the BASE64 decoder will ignore errors + * in the encoded data, returning EOF. This may be useful when dealing + * with improperly encoded messages that contain extraneous data at the + * end of the encoded stream. Note however that errors anywhere in the + * stream will cause the decoder to stop decoding so this should be used + * with extreme caution. The default is false. + *
mail.mime.foldtextboolean + * If set to "true", header fields containing just text + * such as the Subject and Content-Description + * header fields, and long parameter values in structured headers such + * as Content-Type will be folded (broken into 76 character lines) + * when set and unfolded when read. The default is true. + *
mail.mime.setcontenttypefilenameboolean + * If set to "true", the setFileName method + * will also set the name parameter on the Content-Type + * header to the specified filename. This supports interoperability with + * some old mail clients. The default is true. + *
mail.mime.setdefaulttextcharsetboolean + * When updating the headers of a message, a body + * part with a text content type but no charset + * parameter will have a charset parameter added to it + * if this property is set to "true". + * The default is true. + *
mail.mime.parameters.strictboolean + * If set to false, when reading a message, parameter values in header fields + * such as Content-Type and Content-Disposition + * are allowed to contain whitespace and other special characters without + * being quoted; the parameter value ends at the next semicolon. + * If set to true (the default), parameter values are required to conform + * to the MIME specification and must be quoted if they contain whitespace + * or special characters. + *
mail.mime.applefilenamesboolean + * Apple Mail incorrectly encodes filenames that contain spaces, + * forgetting to quote the parameter value. If this property is + * set to "true", Jakarta Mail will try to detect this + * situation when parsing parameters and work around it. + * The default is false. + * Note that this property handles a subset of the cases handled + * by setting the mail.mime.parameters.strict property to false. + * This property will likely be removed in a future release. + *
mail.mime.windowsfilenamesboolean + * Internet Explorer 6 incorrectly includes a complete pathname + * in the filename parameter of the Content-Disposition header + * for uploaded files, and fails to properly escape the backslashes + * in the pathname. If this property is + * set to "true", Jakarta Mail will preserve all backslashes + * in the "filename" and "name" parameters of any MIME header. + * The default is false. + * Note that this is a violation of the MIME specification but may + * be useful when using Jakarta Mail to parse HTTP messages for uploaded + * files sent by IE6. + *
mail.mime. ignoreunknownencodingboolean + * If set to "true", an unknown value in the + * Content-Transfer-Encoding header will be ignored + * when reading a message and an encoding of "8bit" will be assumed. + * If set to "false", an exception is thrown for an + * unknown encoding value. The default is false. + *
mail.mime.uudecode. ignoreerrorsboolean + * If set to "true", errors in the encoded format of a + * uuencoded document will be ignored when reading a message part. + * If set to "false", an exception is thrown for an + * incorrectly encoded message part. The default is false. + *
mail.mime.uudecode. ignoremissingbeginendboolean + * If set to "true", a missing "being" or "end" line in a + * uuencoded document will be ignored when reading a message part. + * If set to "false", an exception is thrown for a + * uuencoded message part without the required "begin" and "end" lines. + * The default is false. + *
mail.mime. ignorewhitespacelinesboolean + * Normally the header of a MIME part is separated from the body by an empty + * line. This System property may be set to "true" to cause + * the parser to consider a line containing only whitespace to be an empty + * line. The default value of this property is false. + *
mail.mime. ignoremultipartencodingboolean + * The MIME spec does not allow body parts of type multipart/* to be encoded. + * The Content-Transfer-Encoding header is ignored in this case. + * Setting this System property to "false" will + * cause the Content-Transfer-Encoding header to be honored for multipart + * content. + * The default value of this property is true. + *
mail.mime.allowencodedmessagesboolean + * The MIME spec does not allow body parts of type message/* to be encoded. + * The Content-Transfer-Encoding header is ignored in this case. + * Some versions of Microsoft Outlook will incorrectly encode message + * attachments. Setting this System property to "true" will + * cause the Content-Transfer-Encoding header to be honored for message + * attachments. + * The default value of this property is false. + *
mail.mime.contenttypehandlerString + * In some cases Jakarta Mail is unable to process messages with an invalid + * Content-Type header. The header may have incorrect syntax or other + * problems. This property specifies the name of a class that will be + * used to clean up the Content-Type header value before Jakarta Mail uses it. + * The class must have a method with this signature: + * public static String cleanContentType(MimePart mp, String contentType) + * Whenever Jakarta Mail accesses the Content-Type header of a message, it + * will pass the value to this method and use the returned value instead. + * The value may be null if the Content-Type header isn't present. + * Returning null will cause the default Content-Type to be used. + * The MimePart may be used to access other headers of the message part + * to determine how to correct the Content-Type. + * Note that the Content-Type handler doesn't affect the + * getHeader method, which still returns the raw header value. + * Note also that the handler doesn't affect the IMAP provider; the IMAP + * server is responsible for returning pre-parsed, syntactically correct + * Content-Type information. + *
mail.mime.address.usecanonicalhostnameboolean + * Use the + * {@link java.net.InetAddress#getCanonicalHostName InetAddress.getCanonicalHostName} + * method to determine the host name in the + * {@link jakarta.mail.internet.InternetAddress#getLocalAddress InternetAddress.getLocalAddress} + * method. + * With some network configurations, InetAddress.getCanonicalHostName may be + * slow or may return an address instead of a host name. + * In that case, setting this System property to false will cause the + * {@link java.net.InetAddress#getHostName() InetAddress.getHostName()} + * method to be used instead. + * The default is true. + *
+ *

+ * The current + * implementation of classes in this package log debugging information using + * {@link java.util.logging.Logger} as described in the following table: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + *
Jakarta Mail Loggers
Logger NameLogging LevelPurpose
jakarta.mail.internetFINEGeneral debugging output
+ */ +package jakarta.mail.internet; \ No newline at end of file diff --git a/net-mail/src/main/java/jakarta/mail/package-info.java b/net-mail/src/main/java/jakarta/mail/package-info.java new file mode 100644 index 0000000..801c203 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/package-info.java @@ -0,0 +1,339 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * The Jakarta Mail API + * provides classes that model a mail system. + * The jakarta.mail package defines classes that are common to + * all mail systems. + * The jakarta.mail.internet package defines classes that are specific + * to mail systems based on internet standards such as MIME, SMTP, POP3, and IMAP. + * The Jakarta Mail API includes the jakarta.mail package and subpackages. + * + *

+ * For an overview of the Jakarta Mail API, read the + * + * Jakarta Mail specification. + *

+ *

+ * The code to send a plain text message can be as simple as the following: + *

+ *
+ * Properties props = new Properties();
+ * props.put("mail.smtp.host", "my-mail-server");
+ * Session session = Session.getInstance(props, null);
+ * 

+ * try { + * MimeMessage msg = new MimeMessage(session); + * msg.setFrom("me@example.com"); + * msg.setRecipients(Message.RecipientType.TO, + * "you@example.com"); + * msg.setSubject("Jakarta Mail hello world example"); + * msg.setSentDate(new Date()); + * msg.setText("Hello, world!\n"); + * Transport.send(msg, "me@example.com", "my-password"); + * } catch (MessagingException mex) { + * System.out.println("send failed, exception: " + mex); + * } + *

+ *

+ * The Jakarta Mail download bundle contains many more complete examples + * in the "demo" directory. + *

+ *

+ * Don't forget to see the + * + * Jakarta Mail API FAQ + * for answers to the most common questions. + * The + * Jakarta Mail web site + * contains many additional resources. + *

+ * Properties + *

+ * The Jakarta Mail API supports the following standard properties, + * which may be set in the Session object, or in the + * Properties object used to create the Session object. + * The properties are always set as strings; the Type column describes + * how the string is interpreted. For example, use + *

+ *
+ * props.put("mail.debug", "true");
+ * 
+ *

+ * to set the mail.debug property, which is of type boolean. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Jakarta Mail properties
NameTypeDescription
mail.debugboolean + * The initial debug mode. + * Default is false. + *
mail.fromString + * The return email address of the current user, used by the + * InternetAddress method getLocalAddress. + *
mail.mime.address.strictboolean + * The MimeMessage class uses the InternetAddress method + * parseHeader to parse headers in messages. This property + * controls the strict flag passed to the parseHeader + * method. The default is true. + *
mail.hostString + * The default host name of the mail server for both Stores and Transports. + * Used if the mail.protocol.host property isn't set. + *
mail.store.protocolString + * Specifies the default message access protocol. The + * Session method getStore() returns a Store + * object that implements this protocol. By default the first Store + * provider in the configuration files is returned. + *
mail.transport.protocolString + * Specifies the default message transport protocol. The + * Session method getTransport() returns a Transport + * object that implements this protocol. By default the first Transport + * provider in the configuration files is returned. + *
mail.userString + * The default user name to use when connecting to the mail server. + * Used if the mail.protocol.user property isn't set. + *
mail.protocol.classString + * Specifies the fully qualified class name of the provider for the + * specified protocol. Used in cases where more than one provider + * for a given protocol exists; this property can be used to specify + * which provider to use by default. The provider must still be listed + * in a configuration file. + *
mail.protocol.hostString + * The host name of the mail server for the specified protocol. + * Overrides the mail.host property. + *
mail.protocol.portint + * The port number of the mail server for the specified protocol. + * If not specified the protocol's default port number is used. + *
mail.protocol.userString + * The user name to use when connecting to mail servers + * using the specified protocol. + * Overrides the mail.user property. + *
+ * + *

+ * The following properties are supported by the EE4J implementation of + * Jakarta Mail, but are not currently a required part of the specification. + * The names, types, defaults, and semantics of these properties may + * change in future releases. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Jakarta Mail implementation properties
NameTypeDescription
mail.debug.authboolean + * Include protocol authentication commands (including usernames and passwords) + * in the debug output. + * Default is false. + *
mail.debug.auth.usernameboolean + * Include the user name in non-protocol debug output. + * Default is true. + *
mail.debug.auth.passwordboolean + * Include the password in non-protocol debug output. + * Default is false. + *
mail.transport.protocol.address-typeString + * Specifies the default message transport protocol for the specified address type. + * The Session method getTransport(Address) returns a + * Transport object that implements this protocol when the address is of the + * specified type (e.g., "rfc822" for standard internet addresses). + * By default the first Transport configured for that address type is used. + * This property can be used to override the behavior of the + * {@link jakarta.mail.Transport#send send} method of the + * {@link jakarta.mail.Transport Transport} class so that (for example) the "smtps" + * protocol is used instead of the "smtp" protocol by setting the property + * mail.transport.protocol.rfc822 to "smtps". + *
mail.event.scopeString + * Controls the scope of events. (See the jakarta.mail.event package.) + * By default, a separate event queue and thread is used for events for each + * Store, Transport, or Folder. + * If this property is set to "session", all such events are put in a single + * event queue processed by a single thread for the current session. + * If this property is set to "application", all such events are put in a single + * event queue processed by a single thread for the current application. + * (Applications are distinguished by their context class loader.) + *
mail.event.executorjava.util.concurrent.Executor + * By default, a new Thread is created for each event queue. + * This thread is used to call the listeners for these events. + * If this property is set to an instance of an Executor, the + * Executor.execute method is used to run the event dispatcher + * for an event queue. The event dispatcher runs until the + * event queue is no longer in use. + *
+ * + *

+ * The Jakarta Mail API also supports several System properties; + * see the {@link jakarta.mail.internet} package documentation + * for details. + *

+ *

+ * The Jakarta Mail reference + * implementation includes protocol providers in subpackages of + * com.sun.mail. Note that the APIs to these protocol + * providers are not part of the standard Jakarta Mail API. Portable + * programs will not use these APIs. + *

+ *

+ * Nonportable programs may use the APIs of the protocol providers + * by (for example) casting a returned Folder object to a + * com.sun.mail.imap.IMAPFolder object. Similarly for + * Store and Message objects returned from the + * standard Jakarta Mail APIs. + *

+ *

+ * The protocol providers also support properties that are specific to + * those providers. The package documentation for the + * {@code com.sun.mail.imap IMAP}, {@code com.sun.mail.pop3 POP3}, + * and {@code com.sun.mail.smtp SMTP} packages provide details. + *

+ *

+ * In addition to printing debugging output as controlled by the + * {@link jakarta.mail.Session Session} configuration, the current + * implementation of classes in this package log the same information using + * {@link java.util.logging.Logger} as described in the following table: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Jakarta Mail Loggers
Logger NameLogging LevelPurpose
jakarta.mailCONFIGConfiguration of the Session
jakarta.mailFINEGeneral debugging output
+ */ +package jakarta.mail; \ No newline at end of file diff --git a/net-mail/src/main/java/jakarta/mail/search/AddressStringTerm.java b/net-mail/src/main/java/jakarta/mail/search/AddressStringTerm.java new file mode 100644 index 0000000..d2bf441 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/AddressStringTerm.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Address; +import jakarta.mail.internet.InternetAddress; + +/** + * This abstract class implements string comparisons for Message + * addresses.

+ *

+ * Note that this class differs from the AddressTerm class + * in that this class does comparisons on address strings rather than + * Address objects. + * + * @since JavaMail 1.1 + */ + +public abstract class AddressStringTerm extends StringTerm { + /** + * Constructor. + * + * @param pattern the address pattern to be compared. + */ + protected AddressStringTerm(String pattern) { + super(pattern, true); // we need case-insensitive comparison. + } + + /** + * Check whether the address pattern specified in the constructor is + * a substring of the string representation of the given Address + * object.

+ *

+ * Note that if the string representation of the given Address object + * contains charset or transfer encodings, the encodings must be + * accounted for, during the match process. + * + * @param a The comparison is applied to this Address object. + * @return true if the match succeeds, otherwise false. + */ + protected boolean match(Address a) { + if (a instanceof InternetAddress) { + InternetAddress ia = (InternetAddress) a; + // We dont use toString() to get "a"'s String representation, + // because InternetAddress.toString() returns a RFC 2047 + // encoded string, which isn't what we need here. + + return super.match(ia.toUnicodeString()); + } else + return super.match(a.toString()); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AddressStringTerm)) + return false; + return super.equals(obj); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/AddressTerm.java b/net-mail/src/main/java/jakarta/mail/search/AddressTerm.java new file mode 100644 index 0000000..a256c55 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/AddressTerm.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Address; + +/** + * This class implements Message Address comparisons. + * + * @author Bill Shannon + * @author John Mani + */ + +public abstract class AddressTerm extends SearchTerm { + /** + * The address. + * + * @serial + */ + protected Address address; + + /** + * Constructor. + * + * @param address the address to match with. + */ + protected AddressTerm(Address address) { + this.address = address; + } + + /** + * Return the address to match with. + * + * @return the adddress + */ + public Address getAddress() { + return address; + } + + /** + * Match against the argument Address. + * + * @param a the address to match + * @return true if it matches + */ + protected boolean match(Address a) { + return (a.equals(address)); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AddressTerm)) + return false; + AddressTerm at = (AddressTerm) obj; + return at.address.equals(this.address); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return address.hashCode(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/AndTerm.java b/net-mail/src/main/java/jakarta/mail/search/AndTerm.java new file mode 100644 index 0000000..4b5d0cd --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/AndTerm.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; + +/** + * This class implements the logical AND operator on individual + * SearchTerms. + * + * @author Bill Shannon + * @author John Mani + */ +public final class AndTerm extends SearchTerm { + + /** + * The array of terms on which the AND operator should be + * applied. + * + * @serial + */ + private SearchTerm[] terms; + + /** + * Constructor that takes two terms. + * + * @param t1 first term + * @param t2 second term + */ + public AndTerm(SearchTerm t1, SearchTerm t2) { + terms = new SearchTerm[2]; + terms[0] = t1; + terms[1] = t2; + } + + /** + * Constructor that takes an array of SearchTerms. + * + * @param t array of terms + */ + public AndTerm(SearchTerm[] t) { + terms = new SearchTerm[t.length]; // clone the array + System.arraycopy(t, 0, terms, 0, t.length); + } + + /** + * Return the search terms. + * + * @return the search terms + */ + public SearchTerm[] getTerms() { + return terms.clone(); + } + + /** + * The AND operation.

+ *

+ * The terms specified in the constructor are applied to + * the given object and the AND operator is applied to their results. + * + * @param msg The specified SearchTerms are applied to this Message + * and the AND operator is applied to their results. + * @return true if the AND succeds, otherwise false + */ + @Override + public boolean match(Message msg) { + for (int i = 0; i < terms.length; i++) + if (!terms[i].match(msg)) + return false; + return true; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof AndTerm)) + return false; + AndTerm at = (AndTerm) obj; + if (at.terms.length != terms.length) + return false; + for (int i = 0; i < terms.length; i++) + if (!terms[i].equals(at.terms[i])) + return false; + return true; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + int hash = 0; + for (int i = 0; i < terms.length; i++) + hash += terms[i].hashCode(); + return hash; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/BodyTerm.java b/net-mail/src/main/java/jakarta/mail/search/BodyTerm.java new file mode 100644 index 0000000..999b3ad --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/BodyTerm.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.Part; +import java.io.IOException; + +/** + * This class implements searches on a message body. + * All parts of the message that are of MIME type "text/*" are searched. + * The pattern is a simple string that must appear as a substring in + * the message body. + * + * @author Bill Shannon + * @author John Mani + */ +public final class BodyTerm extends StringTerm { + + /** + * Constructor + * + * @param pattern The String to search for + */ + public BodyTerm(String pattern) { + // Note: comparison is case-insensitive + super(pattern); + } + + /** + * The match method. + * + * @param msg The pattern search is applied on this Message's body + * @return true if the pattern is found; otherwise false + */ + @Override + public boolean match(Message msg) { + return matchPart(msg); + } + + /** + * Search all the parts of the message for any text part + * that matches the pattern. + */ + private boolean matchPart(Part p) { + try { + /* + * Using isMimeType to determine the content type avoids + * fetching the actual content data until we need it. + */ + if (p.isMimeType("text/*")) { + String s = (String) p.getContent(); + if (s == null) + return false; + /* + * We invoke our superclass' (i.e., StringTerm) match method. + * Note however that StringTerm.match() is not optimized + * for substring searches in large string buffers. We really + * need to have a StringTerm subclass, say BigStringTerm, + * with its own match() method that uses a better algorithm .. + * and then subclass BodyTerm from BigStringTerm. + */ + return super.match(s); + } else if (p.isMimeType("multipart/*")) { + Multipart mp = (Multipart) p.getContent(); + int count = mp.getCount(); + for (int i = 0; i < count; i++) + if (matchPart(mp.getBodyPart(i))) + return true; + } else if (p.isMimeType("message/rfc822")) { + return matchPart((Part) p.getContent()); + } + } catch (MessagingException | RuntimeException | IOException ex) { + } + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof BodyTerm)) + return false; + return super.equals(obj); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/ComparisonTerm.java b/net-mail/src/main/java/jakarta/mail/search/ComparisonTerm.java new file mode 100644 index 0000000..f22ca31 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/ComparisonTerm.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +/** + * This class models the comparison operator. This is an abstract + * class; subclasses implement comparisons for different datatypes. + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class ComparisonTerm extends SearchTerm { + + /** + * Less than or equal to, "{@code <=}", comparison. + */ + public static final int LE = 1; + /** + * Less than, "{@code <}", comparison. + */ + public static final int LT = 2; + /** + * Equal to, "{@code =}", comparison. + */ + public static final int EQ = 3; + /** + * Not equal to, "{@code !=}", comparison. + */ + public static final int NE = 4; + /** + * Greater than, "{@code >}", comparison. + */ + public static final int GT = 5; + /** + * Greater than or equal to, "{@code >=}", comparison. + */ + public static final int GE = 6; + + /** + * The comparison. + * + * @serial + */ + protected int comparison; + + /** + * Creates a default {@code ComparisonTerm}. + * + * @see #comparison + */ + public ComparisonTerm() { + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ComparisonTerm)) + return false; + ComparisonTerm ct = (ComparisonTerm) obj; + return ct.comparison == this.comparison; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return comparison; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/DateTerm.java b/net-mail/src/main/java/jakarta/mail/search/DateTerm.java new file mode 100644 index 0000000..770f315 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/DateTerm.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import java.util.Date; + +/** + * This class implements comparisons for Dates + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class DateTerm extends ComparisonTerm { + /** + * The date. + * + * @serial + */ + protected Date date; + + /** + * Constructor. + * + * @param comparison the comparison type + * @param date The Date to be compared against + */ + protected DateTerm(int comparison, Date date) { + this.comparison = comparison; + this.date = date; + } + + /** + * Return the Date to compare with. + * + * @return the date + */ + public Date getDate() { + return new Date(date.getTime()); + } + + /** + * Return the type of comparison. + * + * @return the comparison type + */ + public int getComparison() { + return comparison; + } + + /** + * The date comparison method. + * + * @param d the date in the constructor is compared with this date + * @return true if the dates match, otherwise false + */ + protected boolean match(Date d) { + switch (comparison) { + case LE: + return d.before(date) || d.equals(date); + case LT: + return d.before(date); + case EQ: + return d.equals(date); + case NE: + return !d.equals(date); + case GT: + return d.after(date); + case GE: + return d.after(date) || d.equals(date); + default: + return false; + } + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof DateTerm)) + return false; + DateTerm dt = (DateTerm) obj; + return dt.date.equals(this.date) && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return date.hashCode() + super.hashCode(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/FlagTerm.java b/net-mail/src/main/java/jakarta/mail/search/FlagTerm.java new file mode 100644 index 0000000..a682524 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/FlagTerm.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Flags; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; + +/** + * This class implements comparisons for Message Flags. + * + * @author Bill Shannon + * @author John Mani + */ +public final class FlagTerm extends SearchTerm { + + /** + * Indicates whether to test for the presence or + * absence of the specified Flag. If true, + * then test whether all the specified flags are present, else + * test whether all the specified flags are absent. + * + * @serial + */ + private boolean set; + + /** + * Flags object containing the flags to test. + * + * @serial + */ + private Flags flags; + + /** + * Constructor. + * + * @param flags Flags object containing the flags to check for + * @param set the flag setting to check for + */ + public FlagTerm(Flags flags, boolean set) { + this.flags = flags; + this.set = set; + } + + /** + * Return the Flags to test. + * + * @return the flags + */ + public Flags getFlags() { + return (Flags) flags.clone(); + } + + /** + * Return true if testing whether the flags are set. + * + * @return true if testing whether the flags are set + */ + public boolean getTestSet() { + return set; + } + + /** + * The comparison method. + * + * @param msg The flag comparison is applied to this Message + * @return true if the comparson succeeds, otherwise false. + */ + @Override + public boolean match(Message msg) { + + try { + Flags f = msg.getFlags(); + if (set) { // This is easy + if (f.contains(flags)) + return true; + else + return false; + } + + // Return true if ALL flags in the passed in Flags + // object are NOT set in this Message. + + // Got to do this the hard way ... + Flags.Flag[] sf = flags.getSystemFlags(); + + // Check each flag in the passed in Flags object + for (int i = 0; i < sf.length; i++) { + if (f.contains(sf[i])) + // this flag IS set in this Message, get out. + return false; + } + + String[] s = flags.getUserFlags(); + + // Check each flag in the passed in Flags object + for (int i = 0; i < s.length; i++) { + if (f.contains(s[i])) + // this flag IS set in this Message, get out. + return false; + } + + return true; + + } catch (MessagingException | RuntimeException e) { + return false; + } + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FlagTerm)) + return false; + FlagTerm ft = (FlagTerm) obj; + return ft.set == this.set && ft.flags.equals(this.flags); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return set ? flags.hashCode() : ~flags.hashCode(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/FromStringTerm.java b/net-mail/src/main/java/jakarta/mail/search/FromStringTerm.java new file mode 100644 index 0000000..acfceed --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/FromStringTerm.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Address; +import jakarta.mail.Message; + +/** + * This class implements string comparisons for the From Address + * header.

+ *

+ * Note that this class differs from the FromTerm class + * in that this class does comparisons on address strings rather than Address + * objects. The string comparisons are case-insensitive. + * + * @since JavaMail 1.1 + */ + +public final class FromStringTerm extends AddressStringTerm { + + /** + * Constructor. + * + * @param pattern the address pattern to be compared. + */ + public FromStringTerm(String pattern) { + super(pattern); + } + + /** + * Check whether the address string specified in the constructor is + * a substring of the From address of this Message. + * + * @param msg The comparison is applied to this Message's From + * address. + * @return true if the match succeeds, otherwise false. + */ + @Override + public boolean match(Message msg) { + Address[] from; + + try { + from = msg.getFrom(); + } catch (Exception e) { + return false; + } + + if (from == null) + return false; + + for (int i = 0; i < from.length; i++) + if (super.match(from[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FromStringTerm)) + return false; + return super.equals(obj); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/FromTerm.java b/net-mail/src/main/java/jakarta/mail/search/FromTerm.java new file mode 100644 index 0000000..99e880a --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/FromTerm.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Address; +import jakarta.mail.Message; + +/** + * This class implements comparisons for the From Address header. + * + * @author Bill Shannon + * @author John Mani + */ +public final class FromTerm extends AddressTerm { + + /** + * Constructor + * + * @param address The Address to be compared + */ + public FromTerm(Address address) { + super(address); + } + + /** + * The address comparator. + * + * @param msg The address comparison is applied to this Message + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Address[] from; + + try { + from = msg.getFrom(); + } catch (Exception e) { + return false; + } + + if (from == null) + return false; + + for (int i = 0; i < from.length; i++) + if (super.match(from[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof FromTerm)) + return false; + return super.equals(obj); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/HeaderTerm.java b/net-mail/src/main/java/jakarta/mail/search/HeaderTerm.java new file mode 100644 index 0000000..736b248 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/HeaderTerm.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; +import java.util.Locale; + +/** + * This class implements comparisons for Message headers. + * The comparison is case-insensitive. + * + * @author Bill Shannon + * @author John Mani + */ +public final class HeaderTerm extends StringTerm { + /** + * The name of the header. + * + * @serial + */ + private String headerName; + + /** + * Constructor. + * + * @param headerName The name of the header + * @param pattern The pattern to search for + */ + public HeaderTerm(String headerName, String pattern) { + super(pattern); + this.headerName = headerName; + } + + /** + * Return the name of the header to compare with. + * + * @return the name of the header + */ + public String getHeaderName() { + return headerName; + } + + /** + * The header match method. + * + * @param msg The match is applied to this Message's header + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + String[] headers; + + try { + headers = msg.getHeader(headerName); + } catch (Exception e) { + return false; + } + + if (headers == null) + return false; + + for (int i = 0; i < headers.length; i++) + if (super.match(headers[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof HeaderTerm)) + return false; + HeaderTerm ht = (HeaderTerm) obj; + // XXX - depends on header comparisons being case independent + return ht.headerName.equalsIgnoreCase(headerName) && super.equals(ht); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + // XXX - depends on header comparisons being case independent + return headerName.toLowerCase(Locale.ENGLISH).hashCode() + + super.hashCode(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/IntegerComparisonTerm.java b/net-mail/src/main/java/jakarta/mail/search/IntegerComparisonTerm.java new file mode 100644 index 0000000..30ccb5d --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/IntegerComparisonTerm.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +/** + * This class implements comparisons for integers. + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class IntegerComparisonTerm extends ComparisonTerm { + /** + * The number. + * + * @serial + */ + protected int number; + + /** + * Constructor. + * + * @param comparison the type of comparison. + * @param number the number to compare with. + */ + protected IntegerComparisonTerm(int comparison, int number) { + this.comparison = comparison; + this.number = number; + } + + /** + * Return the number to compare with. + * + * @return the number + */ + public int getNumber() { + return number; + } + + /** + * Return the type of comparison. + * + * @return the comparison type + */ + public int getComparison() { + return comparison; + } + + /** + * Match against the argument {@code i}. + * + * @param i the integer to match + * @return true if given integer matches this comparison; otherwise false + */ + protected boolean match(int i) { + switch (comparison) { + case LE: + return i <= number; + case LT: + return i < number; + case EQ: + return i == number; + case NE: + return i != number; + case GT: + return i > number; + case GE: + return i >= number; + default: + return false; + } + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof IntegerComparisonTerm)) + return false; + IntegerComparisonTerm ict = (IntegerComparisonTerm) obj; + return ict.number == this.number && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return number + super.hashCode(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/MessageIDTerm.java b/net-mail/src/main/java/jakarta/mail/search/MessageIDTerm.java new file mode 100644 index 0000000..9c2c7e8 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/MessageIDTerm.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; + +/** + * This term models the RFC822 "MessageId" - a message-id for + * Internet messages that is supposed to be unique per message. + * Clients can use this term to search a folder for a message given + * its MessageId.

+ *

+ * The MessageId is represented as a String. + * + * @author Bill Shannon + * @author John Mani + */ +public final class MessageIDTerm extends StringTerm { + + /** + * Constructor. + * + * @param msgid the msgid to search for + */ + public MessageIDTerm(String msgid) { + // Note: comparison is case-insensitive + super(msgid); + } + + /** + * The match method. + * + * @param msg the match is applied to this Message's + * Message-ID header + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + String[] s; + + try { + s = msg.getHeader("Message-ID"); + } catch (Exception e) { + return false; + } + + if (s == null) + return false; + + for (int i = 0; i < s.length; i++) + if (super.match(s[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MessageIDTerm)) + return false; + return super.equals(obj); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/MessageNumberTerm.java b/net-mail/src/main/java/jakarta/mail/search/MessageNumberTerm.java new file mode 100644 index 0000000..3a37ca5 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/MessageNumberTerm.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; + +/** + * This class implements comparisons for Message numbers. + * + * @author Bill Shannon + * @author John Mani + */ +public final class MessageNumberTerm extends IntegerComparisonTerm { + + /** + * Constructor. + * + * @param number the Message number + */ + public MessageNumberTerm(int number) { + super(EQ, number); + } + + /** + * The match method. + * + * @param msg the Message number is matched with this Message + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + int msgno; + + try { + msgno = msg.getMessageNumber(); + } catch (Exception e) { + return false; + } + + return super.match(msgno); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof MessageNumberTerm)) + return false; + return super.equals(obj); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/NotTerm.java b/net-mail/src/main/java/jakarta/mail/search/NotTerm.java new file mode 100644 index 0000000..c532ac9 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/NotTerm.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; + +/** + * This class implements the logical NEGATION operator. + * + * @author Bill Shannon + * @author John Mani + */ +public final class NotTerm extends SearchTerm { + /** + * The search term to negate. + * + * @serial + */ + private SearchTerm term; + + /** + * Constructor. + * + * @param t the term to negate. + */ + public NotTerm(SearchTerm t) { + term = t; + } + + /** + * Return the term to negate. + * + * @return the Term + */ + public SearchTerm getTerm() { + return term; + } + + /* The NOT operation */ + @Override + public boolean match(Message msg) { + return !term.match(msg); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof NotTerm)) + return false; + NotTerm nt = (NotTerm) obj; + return nt.term.equals(this.term); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return term.hashCode() << 1; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/OrTerm.java b/net-mail/src/main/java/jakarta/mail/search/OrTerm.java new file mode 100644 index 0000000..2ca5882 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/OrTerm.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; + +/** + * This class implements the logical OR operator on individual SearchTerms. + * + * @author Bill Shannon + * @author John Mani + */ +public final class OrTerm extends SearchTerm { + + /** + * The array of terms on which the OR operator should + * be applied. + * + * @serial + */ + private SearchTerm[] terms; + + /** + * Constructor that takes two operands. + * + * @param t1 first term + * @param t2 second term + */ + public OrTerm(SearchTerm t1, SearchTerm t2) { + terms = new SearchTerm[2]; + terms[0] = t1; + terms[1] = t2; + } + + /** + * Constructor that takes an array of SearchTerms. + * + * @param t array of search terms + */ + public OrTerm(SearchTerm[] t) { + terms = new SearchTerm[t.length]; + System.arraycopy(t, 0, terms, 0, t.length); + } + + /** + * Return the search terms. + * + * @return the search terms + */ + public SearchTerm[] getTerms() { + return terms.clone(); + } + + /** + * The OR operation.

+ *

+ * The terms specified in the constructor are applied to + * the given object and the OR operator is applied to their results. + * + * @param msg The specified SearchTerms are applied to this Message + * and the OR operator is applied to their results. + * @return true if the OR succeds, otherwise false + */ + + @Override + public boolean match(Message msg) { + for (int i = 0; i < terms.length; i++) + if (terms[i].match(msg)) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof OrTerm)) + return false; + OrTerm ot = (OrTerm) obj; + if (ot.terms.length != terms.length) + return false; + for (int i = 0; i < terms.length; i++) + if (!terms[i].equals(ot.terms[i])) + return false; + return true; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + int hash = 0; + for (int i = 0; i < terms.length; i++) + hash += terms[i].hashCode(); + return hash; + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/ReceivedDateTerm.java b/net-mail/src/main/java/jakarta/mail/search/ReceivedDateTerm.java new file mode 100644 index 0000000..4e41d4f --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/ReceivedDateTerm.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; +import java.util.Date; + +/** + * This class implements comparisons for the Message Received date + * + * @author Bill Shannon + * @author John Mani + */ +public final class ReceivedDateTerm extends DateTerm { + + /** + * Constructor. + * + * @param comparison the Comparison type + * @param date the date to be compared + */ + public ReceivedDateTerm(int comparison, Date date) { + super(comparison, date); + } + + /** + * 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 super.match(d); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof ReceivedDateTerm)) + return false; + return super.equals(obj); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/RecipientStringTerm.java b/net-mail/src/main/java/jakarta/mail/search/RecipientStringTerm.java new file mode 100644 index 0000000..19985ce --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/RecipientStringTerm.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Address; +import jakarta.mail.Message; + +/** + * This class implements string comparisons for the Recipient Address + * headers.

+ *

+ * Note that this class differs from the RecipientTerm class + * in that this class does comparisons on address strings rather than Address + * objects. The string comparisons are case-insensitive. + * + * @since JavaMail 1.1 + */ + +public final class RecipientStringTerm extends AddressStringTerm { + + /** + * The recipient type. + * + * @serial + */ + private Message.RecipientType type; + + /** + * Constructor. + * + * @param type the recipient type + * @param pattern the address pattern to be compared. + */ + public RecipientStringTerm(Message.RecipientType type, String pattern) { + super(pattern); + this.type = type; + } + + /** + * Return the type of recipient to match with. + * + * @return the recipient type + */ + public Message.RecipientType getRecipientType() { + return type; + } + + /** + * Check whether the address specified in the constructor is + * a substring of the recipient address of this Message. + * + * @param msg The comparison is applied to this Message's recipient + * address. + * @return true if the match succeeds, otherwise false. + */ + @Override + public boolean match(Message msg) { + Address[] recipients; + + try { + recipients = msg.getRecipients(type); + } catch (Exception e) { + return false; + } + + if (recipients == null) + return false; + + for (int i = 0; i < recipients.length; i++) + if (super.match(recipients[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RecipientStringTerm)) + return false; + RecipientStringTerm rst = (RecipientStringTerm) obj; + return rst.type.equals(this.type) && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return type.hashCode() + super.hashCode(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/RecipientTerm.java b/net-mail/src/main/java/jakarta/mail/search/RecipientTerm.java new file mode 100644 index 0000000..abf5db0 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/RecipientTerm.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Address; +import jakarta.mail.Message; + +/** + * This class implements comparisons for the Recipient Address headers. + * + * @author Bill Shannon + * @author John Mani + */ +public final class RecipientTerm extends AddressTerm { + + /** + * The recipient type. + * + * @serial + */ + private Message.RecipientType type; + + /** + * Constructor. + * + * @param type the recipient type + * @param address the address to match for + */ + public RecipientTerm(Message.RecipientType type, Address address) { + super(address); + this.type = type; + } + + /** + * Return the type of recipient to match with. + * + * @return the recipient type + */ + public Message.RecipientType getRecipientType() { + return type; + } + + /** + * The match method. + * + * @param msg The address match is applied to this Message's recepient + * address + * @return true if the match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Address[] recipients; + + try { + recipients = msg.getRecipients(type); + } catch (Exception e) { + return false; + } + + if (recipients == null) + return false; + + for (int i = 0; i < recipients.length; i++) + if (super.match(recipients[i])) + return true; + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof RecipientTerm)) + return false; + RecipientTerm rt = (RecipientTerm) obj; + return rt.type.equals(this.type) && super.equals(obj); + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return type.hashCode() + super.hashCode(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/SearchException.java b/net-mail/src/main/java/jakarta/mail/search/SearchException.java new file mode 100644 index 0000000..34c446e --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/SearchException.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.MessagingException; + + +/** + * The exception thrown when a Search expression could not be handled. + * + * @author John Mani + */ +@SuppressWarnings("serial") +public class SearchException extends MessagingException { + + /** + * Constructs a SearchException with no detail message. + */ + public SearchException() { + super(); + } + + /** + * Constructs a SearchException with the specified detail message. + * + * @param s the detail message + */ + public SearchException(String s) { + super(s); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/SearchTerm.java b/net-mail/src/main/java/jakarta/mail/search/SearchTerm.java new file mode 100644 index 0000000..54068cc --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/SearchTerm.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; + +/** + * Search criteria are expressed as a tree of search-terms, forming + * a parse-tree for the search expression.

+ *

+ * Search-terms are represented by this class. This is an abstract + * class; subclasses implement specific match methods.

+ *

+ * Search terms are serializable, which allows storing a search term + * between sessions. + * + * Warning: + * Serialized objects of this class may not be compatible with future + * Jakarta Mail API releases. The current serialization support is + * appropriate for short term storage. + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class SearchTerm { + + /** + * Creates a default {@code SearchTerm}. + */ + public SearchTerm() { + } + + /** + * This method applies a specific match criterion to the given + * message and returns the result. + * + * @param msg The match criterion is applied on this message + * @return true, it the match succeeds, false if the match fails + */ + + public abstract boolean match(Message msg); +} diff --git a/net-mail/src/main/java/jakarta/mail/search/SentDateTerm.java b/net-mail/src/main/java/jakarta/mail/search/SentDateTerm.java new file mode 100644 index 0000000..3bde704 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/SentDateTerm.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; +import java.util.Date; + +/** + * This class implements comparisons for the Message SentDate. + * + * @author Bill Shannon + * @author John Mani + */ +public final class SentDateTerm extends DateTerm { + + /** + * Constructor. + * + * @param comparison the Comparison type + * @param date the date to be compared + */ + public SentDateTerm(int comparison, Date date) { + super(comparison, date); + } + + /** + * The match method. + * + * @param msg the date comparator is applied to this Message's + * sent date + * @return true if the comparison succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + Date d; + + try { + d = msg.getSentDate(); + } catch (Exception e) { + return false; + } + + if (d == null) + return false; + + return super.match(d); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SentDateTerm)) + return false; + return super.equals(obj); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/SizeTerm.java b/net-mail/src/main/java/jakarta/mail/search/SizeTerm.java new file mode 100644 index 0000000..72365fe --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/SizeTerm.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; + +/** + * This class implements comparisons for Message sizes. + * + * @author Bill Shannon + * @author John Mani + */ +public final class SizeTerm extends IntegerComparisonTerm { + + /** + * Constructor. + * + * @param comparison the Comparison type + * @param size the size + */ + public SizeTerm(int comparison, int size) { + super(comparison, size); + } + + /** + * The match method. + * + * @param msg the size comparator is applied to this Message's size + * @return true if the size is equal, otherwise false + */ + @Override + public boolean match(Message msg) { + int size; + + try { + size = msg.getSize(); + } catch (Exception e) { + return false; + } + + if (size == -1) + return false; + + return super.match(size); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SizeTerm)) + return false; + return super.equals(obj); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/StringTerm.java b/net-mail/src/main/java/jakarta/mail/search/StringTerm.java new file mode 100644 index 0000000..63b180f --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/StringTerm.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +/** + * This class implements the match method for Strings. The current + * implementation provides only for substring matching. We + * could add comparisons (like strcmp ...). + * + * @author Bill Shannon + * @author John Mani + */ +public abstract class StringTerm extends SearchTerm { + /** + * The pattern. + * + * @serial + */ + protected String pattern; + + /** + * Ignore case when comparing? + * + * @serial + */ + protected boolean ignoreCase; + + /** + * Construct a StringTerm with the given pattern. + * Case will be ignored. + * + * @param pattern the pattern + */ + protected StringTerm(String pattern) { + this.pattern = pattern; + ignoreCase = true; + } + + /** + * Construct a StringTerm with the given pattern and ignoreCase flag. + * + * @param pattern the pattern + * @param ignoreCase should we ignore case? + */ + protected StringTerm(String pattern, boolean ignoreCase) { + this.pattern = pattern; + this.ignoreCase = ignoreCase; + } + + /** + * Return the string to match with. + * + * @return the string to match + */ + public String getPattern() { + return pattern; + } + + /** + * Return true if we should ignore case when matching. + * + * @return true if we should ignore case + */ + public boolean getIgnoreCase() { + return ignoreCase; + } + + /** + * The match method. + * + * @param s The pattern search is applied on given String + * @return true if given string matches this pattern; otherwise false + */ + protected boolean match(String s) { + int len = s.length() - pattern.length(); + for (int i = 0; i <= len; i++) { + if (s.regionMatches(ignoreCase, i, + pattern, 0, pattern.length())) + return true; + } + return false; + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StringTerm)) + return false; + StringTerm st = (StringTerm) obj; + if (ignoreCase) + return st.pattern.equalsIgnoreCase(this.pattern) && + st.ignoreCase == this.ignoreCase; + else + return st.pattern.equals(this.pattern) && + st.ignoreCase == this.ignoreCase; + } + + /** + * Compute a hashCode for this object. + */ + @Override + public int hashCode() { + return ignoreCase ? pattern.hashCode() : ~pattern.hashCode(); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/SubjectTerm.java b/net-mail/src/main/java/jakarta/mail/search/SubjectTerm.java new file mode 100644 index 0000000..44c5634 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/SubjectTerm.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.search; + +import jakarta.mail.Message; + +/** + * This class implements comparisons for the message Subject header. + * The comparison is case-insensitive. The pattern is a simple string + * that must appear as a substring in the Subject. + * + * @author Bill Shannon + * @author John Mani + */ +public final class SubjectTerm extends StringTerm { + /** + * Constructor. + * + * @param pattern the pattern to search for + */ + public SubjectTerm(String pattern) { + // Note: comparison is case-insensitive + super(pattern); + } + + /** + * The match method. + * + * @param msg the pattern match is applied to this Message's + * subject header + * @return true if the pattern match succeeds, otherwise false + */ + @Override + public boolean match(Message msg) { + String subj; + + try { + subj = msg.getSubject(); + } catch (Exception e) { + return false; + } + + if (subj == null) + return false; + + return super.match(subj); + } + + /** + * Equality comparison. + */ + @Override + public boolean equals(Object obj) { + if (!(obj instanceof SubjectTerm)) + return false; + return super.equals(obj); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/search/package-info.java b/net-mail/src/main/java/jakarta/mail/search/package-info.java new file mode 100644 index 0000000..337834a --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/search/package-info.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * Message search terms for the Jakarta Mail API. + * This package defines classes that can be used to construct a search + * expression to search a folder for messages matching the expression; + * see the {@link jakarta.mail.Folder#search search} method on + * {@link jakarta.mail.Folder jakarta.mail.Folder}. + * See {@link jakarta.mail.search.SearchTerm SearchTerm}. + * + *

+ * Note that the exact search capabilities depend on the protocol, + * provider, and server in use. For the POP3 protocol, all searching is + * done on the client side using the Jakarta Mail classes. For IMAP, all + * searching is done on the server side and is limited by the search + * capabilities of the IMAP protocol and the IMAP server being used. + * For example, IMAP date based searches have only day granularity. + *

+ *

+ * In general, all of the string patterns supported by search terms are + * just simple strings; no regular expressions are supported. + */ +package jakarta.mail.search; \ No newline at end of file diff --git a/net-mail/src/main/java/jakarta/mail/util/ByteArrayDataSource.java b/net-mail/src/main/java/jakarta/mail/util/ByteArrayDataSource.java new file mode 100644 index 0000000..9e5b7ff --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/util/ByteArrayDataSource.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.util; + +import jakarta.activation.DataSource; +import jakarta.mail.internet.ContentType; +import jakarta.mail.internet.MimeUtility; +import jakarta.mail.internet.ParseException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * A DataSource backed by a byte array. The byte array may be + * passed in directly, or may be initialized from an InputStream + * or a String. + * + * @author John Mani + * @author Bill Shannon + * @author Max Spivak + * @since JavaMail 1.4 + */ +public class ByteArrayDataSource implements DataSource { + private byte[] data; // data + private int len = -1; + private String type; // content-type + private String name = ""; + + /** + * Create a ByteArrayDataSource with data from the + * specified InputStream and with the specified MIME type. + * The InputStream is read completely and the data is + * stored in a byte array. + * + * @param is the InputStream + * @param type the MIME type + * @throws IOException errors reading the stream + */ + public ByteArrayDataSource(InputStream is, String type) throws IOException { + DSByteArrayOutputStream os = new DSByteArrayOutputStream(); + byte[] buf = new byte[8192]; + int len; + while ((len = is.read(buf)) > 0) + os.write(buf, 0, len); + this.data = os.getBuf(); + this.len = os.getCount(); + + /* + * ByteArrayOutputStream doubles the size of the buffer every time + * it needs to expand, which can waste a lot of memory in the worst + * case with large buffers. Check how much is wasted here and if + * it's too much, copy the data into a new buffer and allow the + * old buffer to be garbage collected. + */ + if (this.data.length - this.len > 256 * 1024) { + this.data = os.toByteArray(); + this.len = this.data.length; // should be the same + } + this.type = type; + } + + /** + * Create a ByteArrayDataSource with data from the + * specified byte array and with the specified MIME type. + * + * @param data the data + * @param type the MIME type + */ + public ByteArrayDataSource(byte[] data, String type) { + this.data = data; + this.type = type; + } + + /** + * Create a ByteArrayDataSource with data from the + * specified String and with the specified MIME type. + * The MIME type should include a charset + * parameter specifying the charset to be used for the + * string. If the parameter is not included, the + * default charset is used. + * + * @param data the String + * @param type the MIME type + * @throws IOException errors reading the String + */ + public ByteArrayDataSource(String data, String type) throws IOException { + String charset = null; + try { + ContentType ct = new ContentType(type); + charset = ct.getParameter("charset"); + } catch (ParseException pex) { + // ignore parse error + } + charset = MimeUtility.javaCharset(charset); + if (charset == null) + charset = MimeUtility.getDefaultJavaCharset(); + // XXX - could convert to bytes on demand rather than copying here + this.data = data.getBytes(charset); + this.type = type; + } + + /** + * Return an InputStream for the data. + * Note that a new stream is returned each time + * this method is called. + * + * @return the InputStream + * @throws IOException if no data has been set + */ + @Override + public InputStream getInputStream() throws IOException { + if (data == null) + throw new IOException("no data"); + if (len < 0) + len = data.length; + return new SharedByteArrayInputStream(data, 0, len); + } + + /** + * Return an OutputStream for the data. + * Writing the data is not supported; an IOException + * is always thrown. + * + * @throws IOException always + */ + @Override + public OutputStream getOutputStream() throws IOException { + throw new IOException("cannot do this"); + } + + /** + * Get the MIME content type of the data. + * + * @return the MIME type + */ + @Override + public String getContentType() { + return type; + } + + /** + * Get the name of the data. + * By default, an empty string ("") is returned. + * + * @return the name of this data + */ + @Override + public String getName() { + return name; + } + + /** + * Set the name of the data. + * + * @param name the name of this data + */ + public void setName(String name) { + this.name = name; + } + + static class DSByteArrayOutputStream extends ByteArrayOutputStream { + public byte[] getBuf() { + return buf; + } + + public int getCount() { + return count; + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/util/FactoryFinder.java b/net-mail/src/main/java/jakarta/mail/util/FactoryFinder.java new file mode 100644 index 0000000..87a1829 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/util/FactoryFinder.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2021, 2023 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 jakarta.mail.util; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.ServiceLoader; + +class FactoryFinder { + + /** + * Finds the implementation {@code Class} object for the given + * factory type. + * The arguments supplied must be used in order + * This method is package private so that this code can be shared. + * + * @param factoryClass factory abstract class or interface to be found + * @return the {@code Class} object of the specified message factory + * @throws RuntimeException if there is an error + */ + static T find(Class factoryClass) throws RuntimeException { + T result; + ClassLoader loader = factoryClass.getClassLoader(); + if (loader != null) { + result = find(factoryClass, loader); + if (result != null) { + return result; + } + } + result = find(factoryClass, ClassLoader.getSystemClassLoader()); + if (result != null) { + return result; + } + + throw new IllegalStateException("No provider of " + factoryClass.getName() + " was found"); + } + + private static T find(Class factoryClass, ClassLoader loader) throws RuntimeException { + // Use the system property first + String className = fromSystemProperty(factoryClass.getName()); + if (className != null) { + T result = newInstance(className, factoryClass, loader); + if (result != null) { + return result; + } + } + + // standard services: java.util.ServiceLoader + return factoryFromServiceLoader(factoryClass, loader); + } + + private static T newInstance(String className, Class factoryClass, ClassLoader classLoader) throws RuntimeException { + try { + Class clazz; + if (classLoader == null) { //Match behavior of ServiceLoader + classLoader = ClassLoader.getSystemClassLoader(); + } + clazz = Class.forName(className, false, classLoader); + return clazz.asSubclass(factoryClass).getConstructor().newInstance(); + } catch (ClassCastException wrongLoader) { + return null; + } catch (ReflectiveOperationException e) { + throw new IllegalArgumentException("Cannot instance " + className, e); + } + } + + private static String fromSystemProperty(String factoryId) { + return System.getProperty(factoryId); + } + + private static T factoryFromServiceLoader(Class factory, ClassLoader loader) { + try { + ServiceLoader sl = ServiceLoader.load(factory, loader); + Iterator iter = sl.iterator(); + if (iter.hasNext()) { + return factory.cast(iter.next()); //Verify loader + } else { + return null; + } + } catch (ClassCastException wrongLoader) { + return null; + } catch (Throwable t) { + throw new IllegalStateException("Cannot load " + factory + " as ServiceLoader", t); + } + } +} + diff --git a/net-mail/src/main/java/jakarta/mail/util/LineInputStream.java b/net-mail/src/main/java/jakarta/mail/util/LineInputStream.java new file mode 100644 index 0000000..705bb31 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/util/LineInputStream.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 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 jakarta.mail.util; + +import java.io.IOException; + +/** + * 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. + */ +public interface LineInputStream { + + /** + * 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. + * + * @return the line + * @throws IOException for I/O errors + */ + String readLine() throws IOException; + +} diff --git a/net-mail/src/main/java/jakarta/mail/util/LineOutputStream.java b/net-mail/src/main/java/jakarta/mail/util/LineOutputStream.java new file mode 100644 index 0000000..a970b4d --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/util/LineOutputStream.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2021 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 jakarta.mail.util; + +import java.io.IOException; + +/** + * This interface 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. + */ +public interface LineOutputStream { + + /** + * Writes the input string and a new line (CRLF). + * + * @param s the string to write before the new line. + * @throws IOException if an I/O error occurs. + */ + void writeln(String s) throws IOException; + + /** + * Writes a new line (CRLF). + * + * @throws IOException if an I/O error occurs. + */ + void writeln() throws IOException; + + /** + * Writes b.length bytes to this output stream. + * + * @param content the content to write. + * @throws IOException if an I/O error occurs. + */ + void write(byte[] content) throws IOException; + +} diff --git a/net-mail/src/main/java/jakarta/mail/util/SharedByteArrayInputStream.java b/net-mail/src/main/java/jakarta/mail/util/SharedByteArrayInputStream.java new file mode 100644 index 0000000..92c5146 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/util/SharedByteArrayInputStream.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.util; + +import jakarta.mail.internet.SharedInputStream; +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * A ByteArrayInputStream that implements the SharedInputStream interface, + * allowing the underlying byte array to be shared between multiple readers. + * + * @author Bill Shannon + * @since JavaMail 1.4 + */ + +public class SharedByteArrayInputStream extends ByteArrayInputStream + implements SharedInputStream { + /** + * Position within shared buffer that this stream starts at. + */ + protected int start = 0; + + /** + * Create a SharedByteArrayInputStream representing the entire + * byte array. + * + * @param buf the byte array + */ + public SharedByteArrayInputStream(byte[] buf) { + super(buf); + } + + /** + * Create a SharedByteArrayInputStream representing the part + * of the byte array from offset for length + * bytes. + * + * @param buf the byte array + * @param offset offset in byte array to first byte to include + * @param length number of bytes to include + */ + public SharedByteArrayInputStream(byte[] buf, int offset, int length) { + super(buf, offset, length); + start = offset; + } + + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + @Override + public long getPosition() { + return pos - start; + } + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at start (inclusive) + * up to end (exclusive). start must be + * non-negative. If end is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + @Override + public InputStream newStream(long start, long end) { + if (start < 0) + throw new IllegalArgumentException("start < 0"); + if (end == -1) + end = count - this.start; + return new SharedByteArrayInputStream(buf, + this.start + (int) start, (int) (end - start)); + } +} diff --git a/net-mail/src/main/java/jakarta/mail/util/SharedFileInputStream.java b/net-mail/src/main/java/jakarta/mail/util/SharedFileInputStream.java new file mode 100644 index 0000000..83ec229 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/util/SharedFileInputStream.java @@ -0,0 +1,480 @@ +/* + * Copyright (c) 1997, 2023 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 jakarta.mail.util; + +import jakarta.mail.internet.SharedInputStream; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.RandomAccessFile; +import java.util.Objects; + +/** + * A SharedFileInputStream is a + * BufferedInputStream that buffers + * data from the file and supports the mark + * and reset methods. It also supports the + * newStream method that allows you to create + * other streams that represent subsets of the file. + * A RandomAccessFile object is used to + * access the file data.

+ * + * @author Bill Shannon + * @since JavaMail 1.4 + */ +public class SharedFileInputStream extends BufferedInputStream + implements SharedInputStream { + + private static int defaultBufferSize = 2048; + + /** + * The file containing the data. + * Shared by all related SharedFileInputStreams. + */ + protected RandomAccessFile in; + + /** + * The normal size of the read buffer. + */ + protected int bufsize; + + /** + * The file offset that corresponds to the first byte in + * the read buffer. + */ + protected long bufpos; + + /** + * The file offset of the start of data in this subset of the file. + */ + protected long start = 0; + + /** + * The amount of data in this subset of the file. + */ + protected long datalen; + private SharedFile sf; + + /** + * Creates a SharedFileInputStream + * for the file. + * + * @param file the file + * @throws IOException for errors opening the file + */ + public SharedFileInputStream(File file) throws IOException { + this(file, defaultBufferSize); + } + + /** + * Creates a SharedFileInputStream + * for the named file + * + * @param file the file + * @throws IOException for errors opening the file + */ + public SharedFileInputStream(String file) throws IOException { + this(file, defaultBufferSize); + } + + /** + * Creates a SharedFileInputStream + * with the specified buffer size. + * + * @param file the file + * @param size the buffer size. + * @throws IOException for errors opening the file + * @throws IllegalArgumentException if size ≤ 0. + */ + public SharedFileInputStream(File file, int size) throws IOException { + super(null); // XXX - will it NPE? + if (size <= 0) + throw new IllegalArgumentException("Buffer size <= 0"); + init(new SharedFile(file), size); + } + + /** + * Creates a SharedFileInputStream + * with the specified buffer size. + * + * @param file the file + * @param size the buffer size. + * @throws IOException for errors opening the file + * @throws IllegalArgumentException if size ≤ 0. + */ + public SharedFileInputStream(String file, int size) throws IOException { + super(null); // XXX - will it NPE? + if (size <= 0) + throw new IllegalArgumentException("Buffer size <= 0"); + init(new SharedFile(file), size); + } + + /** + * Used internally by the newStream method. + */ + private SharedFileInputStream(SharedFile sf, long start, long len, + int bufsize) { + super(null); + this.sf = sf; + this.in = sf.open(); + this.start = start; + this.bufpos = start; + this.datalen = len; + this.bufsize = bufsize; + buf = new byte[bufsize]; + } + + /** + * Check to make sure that this stream has not been closed + */ + private void ensureOpen() throws IOException { + if (in == null) + throw new IOException("Stream closed"); + } + + private void init(SharedFile sf, int size) throws IOException { + this.sf = sf; + this.in = sf.open(); + this.start = 0; + this.datalen = in.length(); // XXX - file can't grow + this.bufsize = size; + buf = new byte[size]; + } + + /** + * Fills the buffer with more data, taking into account + * shuffling and other tricks for dealing with marks. + * Assumes that it is being called by a synchronized method. + * This method also assumes that all data has already been read in, + * hence pos > count. + */ + private void fill() throws IOException { + if (markpos < 0) { + pos = 0; /* no mark: throw away the buffer */ + bufpos += count; + } else if (pos >= buf.length) /* no room left in buffer */ + if (markpos > 0) { /* can throw away early part of the buffer */ + int sz = pos - markpos; + System.arraycopy(buf, markpos, buf, 0, sz); + pos = sz; + bufpos += markpos; + markpos = 0; + } else if (buf.length >= marklimit) { + markpos = -1; /* buffer got too big, invalidate mark */ + pos = 0; /* drop buffer contents */ + bufpos += count; + } else { /* grow buffer */ + int nsz = pos * 2; + if (nsz > marklimit) + nsz = marklimit; + byte[] nbuf = new byte[nsz]; + System.arraycopy(buf, 0, nbuf, 0, pos); + buf = nbuf; + } + count = pos; + // limit to datalen + int len = buf.length - pos; + if (bufpos - start + pos + len > datalen) + len = (int) (datalen - (bufpos - start + pos)); + synchronized (in) { + in.seek(bufpos + pos); + int n = in.read(buf, pos, len); + if (n > 0) + count = n + pos; + } + } + + /** + * See the general contract of the read + * method of InputStream. + * + * @return the next byte of data, or -1 if the end of the + * stream is reached. + * @throws IOException if an I/O error occurs. + */ + @Override + public synchronized int read() throws IOException { + ensureOpen(); + if (pos >= count) { + fill(); + if (pos >= count) + return -1; + } + return buf[pos++] & 0xff; + } + + /** + * Read characters into a portion of an array, reading from the underlying + * stream at most once if necessary. + */ + private int read1(byte[] b, int off, int len) throws IOException { + int avail = count - pos; + if (avail <= 0) { + if (false) { + /* If the requested length is at least as large as the buffer, and + if there is no mark/reset activity, do not bother to copy the + bytes into the local buffer. In this way buffered streams will + cascade harmlessly. */ + if (len >= buf.length && markpos < 0) { + // XXX - seek, update bufpos - how? + return in.read(b, off, len); + } + } + fill(); + avail = count - pos; + if (avail <= 0) return -1; + } + int cnt = Math.min(avail, len); + System.arraycopy(buf, pos, b, off, cnt); + pos += cnt; + return cnt; + } + + /** + * Reads bytes from this stream into the specified byte array, + * starting at the given offset. + * + *

This method implements the general contract of the corresponding + * {@link InputStream#read(byte[], int, int) read} + * method of the {@link InputStream} class. + * + * @param b destination buffer. + * @param off offset at which to start storing bytes. + * @param len maximum number of bytes to read. + * @return the number of bytes read, or -1 if the end of + * the stream has been reached. + * @throws IOException if an I/O error occurs. + */ + @Override + public synchronized int read(byte[] b, int off, int len) + throws IOException { + ensureOpen(); + if ((off | len | (off + len) | (b.length - (off + len))) < 0) { + throw new IndexOutOfBoundsException(); + } else if (len == 0) { + return 0; + } + + int n = read1(b, off, len); + if (n <= 0) return n; + while ((n < len) /* && (in.available() > 0) */) { + int n1 = read1(b, off + n, len - n); + if (n1 <= 0) break; + n += n1; + } + return n; + } + + /** + * See the general contract of the skip + * method of InputStream. + * + * @param n the number of bytes to be skipped. + * @return the actual number of bytes skipped. + * @throws IOException if an I/O error occurs. + */ + @Override + public synchronized long skip(long n) throws IOException { + ensureOpen(); + if (n <= 0) { + return 0; + } + long avail = count - pos; + + if (avail <= 0) { + // If no mark position set then don't keep in buffer + /* + if (markpos <0) + return in.skip(n); + */ + + // Fill in buffer to save bytes for reset + fill(); + avail = count - pos; + if (avail <= 0) + return 0; + } + + long skipped = Math.min(avail, n); + pos += (int)skipped; + return skipped; + } + + /** + * Returns the number of bytes that can be read from this input + * stream without blocking. + * + * @return the number of bytes that can be read from this input + * stream without blocking. + * @throws IOException if an I/O error occurs. + */ + @Override + public synchronized int available() throws IOException { + ensureOpen(); + return (count - pos) + in_available(); + } + + private int in_available() throws IOException { + // XXX - overflow + return (int) ((start + datalen) - (bufpos + count)); + } + + /** + * See the general contract of the mark + * method of InputStream. + * + * @param readlimit the maximum limit of bytes that can be read before + * the mark position becomes invalid. + * @see #reset() + */ + @Override + public synchronized void mark(int readlimit) { + marklimit = readlimit; + markpos = pos; + } + + /** + * See the general contract of the reset + * method of InputStream. + *

+ * If markpos is -1 + * (no mark has been set or the mark has been + * invalidated), an IOException + * is thrown. Otherwise, pos is + * set equal to markpos. + * + * @throws IOException if this stream has not been marked or + * if the mark has been invalidated. + * @see #mark(int) + */ + @Override + public synchronized void reset() throws IOException { + ensureOpen(); + if (markpos < 0) + throw new IOException("Resetting to invalid mark"); + pos = markpos; + } + + /** + * Tests if this input stream supports the mark + * and reset methods. The markSupported + * method of SharedFileInputStream returns + * true. + * + * @return a boolean indicating if this stream type supports + * the mark and reset methods. + * @see InputStream#mark(int) + * @see InputStream#reset() + */ + @Override + public boolean markSupported() { + return true; + } + + /** + * Closes this input stream and releases any system resources + * associated with the stream. + * + * @throws IOException if an I/O error occurs. + */ + @Override + public void close() throws IOException { + if (in == null) + return; + try { + sf.close(); + } finally { + sf = null; + in = null; + buf = null; + Objects.requireNonNull(this); //TODO: replace with Reference.reachabilityFence + } + } + + /** + * Return the current position in the InputStream, as an + * offset from the beginning of the InputStream. + * + * @return the current position + */ + @Override + public long getPosition() { +//System.out.println("getPosition: start " + start + " pos " + pos +// + " bufpos " + bufpos + " = " + (bufpos + pos - start)); + if (in == null) + throw new RuntimeException("Stream closed"); + return bufpos + pos - start; + } + + /** + * Return a new InputStream representing a subset of the data + * from this InputStream, starting at start (inclusive) + * up to end (exclusive). start must be + * non-negative. If end is -1, the new stream ends + * at the same place as this stream. The returned InputStream + * will also implement the SharedInputStream interface. + * + * @param start the starting position + * @param end the ending position + 1 + * @return the new stream + */ + @Override + public synchronized InputStream newStream(long start, long end) { + try { + if (in == null) + throw new RuntimeException("Stream closed"); + if (start < 0) + throw new IllegalArgumentException("start < 0"); + if (end == -1) + end = datalen; + + return new SharedFileInputStream(sf, + this.start + start, end - start, bufsize); + } finally { + Objects.requireNonNull(this); //TODO: replace with Reference.reachabilityFence + } + } + + /** + * A shared class that keeps track of the references + * to a particular file so it can be closed when the + * last reference is gone. + */ + static class SharedFile { + private int cnt; + private RandomAccessFile in; + + SharedFile(String file) throws IOException { + this.in = new RandomAccessFile(file, "r"); + } + + SharedFile(File file) throws IOException { + this.in = new RandomAccessFile(file, "r"); + } + + public synchronized RandomAccessFile open() { + cnt++; + return in; + } + + public synchronized void close() throws IOException { + if (cnt > 0 && --cnt <= 0) + in.close(); + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/util/StreamProvider.java b/net-mail/src/main/java/jakarta/mail/util/StreamProvider.java new file mode 100644 index 0000000..0eac1cf --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/util/StreamProvider.java @@ -0,0 +1,192 @@ +/* + * Copyright (c) 2021, 2023 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 jakarta.mail.util; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ServiceLoader; + +/** + * Service lookup is used to find implementations of this interface. + *

+ * It contains the methods to instance different encoders/decoders and + * other streams required by the API. + * + * @since JavaMail 2.1 + */ +public interface StreamProvider { + + /** + * Creates a stream provider object. The provider is loaded using the + * {@link ServiceLoader#load(Class)} method. If there are no available + * service providers, this method throws an IllegalStateException. + * Users are recommended to cache the result of this method. + * + * @return a stream provider + */ + static StreamProvider provider() { + return FactoryFinder.find(StreamProvider.class); + } + + /** + * Creates a 'base64' decoder from the InputStream. + * + * @param in the InputStream + * @return the decoder + */ + InputStream inputBase64(InputStream in); + + /** + * Creates a 'base64' encoder from the OutputStream. + * + * @param out the OutputStream + * @return the encoder + */ + OutputStream outputBase64(OutputStream out); + + /** + * Creates a 'binary', '7bit' and '8bit' decoder from the InputStream. + * + * @param in the InputStream + * @return the decoder + */ + InputStream inputBinary(InputStream in); + + /** + * Creates a 'binary', '7bit' and '8bit' encoder from the OutputStream. + * + * @param out the OutputStream + * @return the encoder + */ + OutputStream outputBinary(OutputStream out); + + /** + * Creates a 'b' encoder from the OutputStream. + * + * @param out the OutputStream + * @return the encoder + */ + OutputStream outputB(OutputStream out); + + /** + * Creates a 'q' decoder from the InputStream. + * + * @param in the InputStream + * @return the decoder + */ + InputStream inputQ(InputStream in); + + /** + * Creates a 'q' encoder. + * + * @param out the OutputStream + * @param encodingWord true if we are Q-encoding a word within a phrase. + * @return the encoder + */ + OutputStream outputQ(OutputStream out, boolean encodingWord); + + /** + * Creates a new LineInputStream that supports reading CRLF terminated lines + * containing only US-ASCII characters from an input stream + * + * @param in the InputStream + * @param allowutf8 allow UTF-8 characters? + * @return the LineInputStream + */ + LineInputStream inputLineStream(InputStream in, boolean allowutf8); + + /** + * Creates a new LineOutputStream that supports writing out Strings as a sequence of bytes terminated + * by a CRLF sequence. The String must contain only US-ASCII characters. + * + * @param out the OutputStream + * @param allowutf8 allow UTF-8 characters? + * @return the LineOutputStream + */ + LineOutputStream outputLineStream(OutputStream out, boolean allowutf8); + + /** + * Creates a 'quoted-printable' decoder from the InputStream. + * + * @param in the InputStream + * @return the decoder + */ + InputStream inputQP(InputStream in); + + /** + * Creates a 'quoted-printable' encoder from the OutputStream. + * + * @param out the OutputStream + * @return the encoder + */ + OutputStream outputQP(OutputStream out); + + /** + * Creates a new InputStream from the underlying byte array to be shared + * between multiple readers. + * + * @param buff the byte array + * @return the InputStream + */ + InputStream inputSharedByteArray(byte[] buff); + + /** + * Creates a 'uuencode', 'x-uuencode' and 'x-uue' decoder from the InputStream. + * + * @param in the InputStream + * @return the decoder + */ + InputStream inputUU(InputStream in); + + /** + * Creates a 'uuencode', 'x-uuencode' and 'x-uue' encoder from the OutputStream. + * + * @param out the OutputStream + * @param filename Specifies a name for the encoded buffer. It can be null. + * @return the encoder + */ + OutputStream outputUU(OutputStream out, String filename); + + /** + * Enumeration with the different encoder types supported by the Mail API. + * + * @since JavaMail 2.1 + */ + enum EncoderTypes { + + BASE_64("base64"), + B_ENCODER("b"), + Q_ENCODER("q"), + BINARY_ENCODER("binary"), + BIT7_ENCODER("7bit"), + BIT8_ENCODER("8bit"), + QUOTED_PRINTABLE_ENCODER("quoted-printable"), + UU_ENCODER("uuencode"), + X_UU_ENCODER("x-uuencode"), + X_UUE("x-uue"); + + private final String encoder; + + EncoderTypes(String encoder) { + this.encoder = encoder; + } + + public String getEncoder() { + return encoder; + } + } +} diff --git a/net-mail/src/main/java/jakarta/mail/util/package-info.java b/net-mail/src/main/java/jakarta/mail/util/package-info.java new file mode 100644 index 0000000..9a3e182 --- /dev/null +++ b/net-mail/src/main/java/jakarta/mail/util/package-info.java @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * Jakarta Mail API utility classes. + * This package specifies utility classes that are useful with + * other Jakarta Mail APIs. + */ +package jakarta.mail.util; \ No newline at end of file diff --git a/net-mail/src/main/java/module-info.java b/net-mail/src/main/java/module-info.java new file mode 100644 index 0000000..966fe9f --- /dev/null +++ b/net-mail/src/main/java/module-info.java @@ -0,0 +1,37 @@ +module org.xbib.net.mail { + requires java.logging; + requires java.xml; + requires java.security.sasl; + requires org.xbib.net.security.auth; + exports jakarta.activation; + exports jakarta.activation.spi; + exports jakarta.mail; + exports jakarta.mail.event; + exports jakarta.mail.internet; + exports jakarta.mail.search; + exports jakarta.mail.util; + exports org.xbib.net.mail.handlers; + exports org.xbib.net.mail.iap; + exports org.xbib.net.mail.imap; + exports org.xbib.net.mail.imap.protocol; + exports org.xbib.net.mail.mbox; + exports org.xbib.net.mail.pop3; + exports org.xbib.net.mail.remote; + exports org.xbib.net.mail.smtp; + exports org.xbib.net.mail.util; + opens org.xbib.net.mail.iap to org.xbib.net.mail.test; + opens org.xbib.net.mail.util to org.xbib.net.mail.test; + uses jakarta.activation.spi.MailcapRegistryProvider; + uses jakarta.activation.spi.MimeTypeRegistryProvider; + uses jakarta.mail.Provider; + uses jakarta.mail.util.StreamProvider; + provides jakarta.mail.util.StreamProvider with + org.xbib.net.mail.util.MailStreamProvider; + provides jakarta.mail.Provider with + org.xbib.net.mail.imap.IMAPProvider, + org.xbib.net.mail.imap.IMAPSSLProvider, + org.xbib.net.mail.smtp.SMTPProvider, + org.xbib.net.mail.smtp.SMTPSSLProvider, + org.xbib.net.mail.pop3.POP3Provider, + org.xbib.net.mail.pop3.POP3SSLProvider; +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/dsn/DeliveryStatus.java b/net-mail/src/main/java/org/xbib/net/mail/dsn/DeliveryStatus.java new file mode 100644 index 0000000..e86fc9f --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/dsn/DeliveryStatus.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.dsn; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.InternetHeaders; +import jakarta.mail.util.LineOutputStream; +import jakarta.mail.util.StreamProvider; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; +import java.util.Vector; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * A message/delivery-status message content, as defined in + * RFC 3464. + * + * @since JavaMail 1.4 + */ +public class DeliveryStatus extends Report { + + private static final Logger logger = Logger.getLogger(DeliveryStatus.class.getName()); + + /** + * The DSN fields for the message. + */ + protected InternetHeaders messageDSN; + + /** + * The DSN fields for each recipient. + */ + protected InternetHeaders[] recipientDSN; + + /** + * Construct a delivery status notification with no content. + * + * @exception MessagingException for failures + */ + public DeliveryStatus() throws MessagingException { + super("delivery-status"); + messageDSN = new InternetHeaders(); + recipientDSN = new InternetHeaders[0]; + } + + /** + * Construct a delivery status notification by parsing the + * supplied input stream. + * + * @param is the input stream + * @exception IOException for I/O errors reading the stream + * @exception MessagingException for other failures + */ + public DeliveryStatus(InputStream is) + throws MessagingException, IOException { + super("delivery-status"); + messageDSN = new InternetHeaders(is); + logger.fine("got messageDSN"); + Vector v = new Vector<>(); + try { + while (is.available() > 0) { + InternetHeaders h = new InternetHeaders(is); + logger.fine("got recipientDSN"); + v.addElement(h); + } + } catch (EOFException ex) { + logger.log(Level.FINE, "got EOFException", ex); + } + if (logger.isLoggable(Level.FINE)) + logger.fine("recipientDSN size " + v.size()); + recipientDSN = new InternetHeaders[v.size()]; + v.copyInto(recipientDSN); + } + + /** + * Return all the per-message fields in the delivery status notification. + * The fields are defined as: + * + *

+     *    per-message-fields =
+     *          [ original-envelope-id-field CRLF ]
+     *          reporting-mta-field CRLF
+     *          [ dsn-gateway-field CRLF ]
+     *          [ received-from-mta-field CRLF ]
+     *          [ arrival-date-field CRLF ]
+     *          *( extension-field CRLF )
+     * 
+ * + * @return the per-message DSN fields + */ + // XXX - could parse each of these fields + public InternetHeaders getMessageDSN() { + return messageDSN; + } + + /** + * Set the per-message fields in the delivery status notification. + * + * @param messageDSN the per-message DSN fields + */ + public void setMessageDSN(InternetHeaders messageDSN) { + this.messageDSN = messageDSN; + } + + /** + * Return the number of recipients for which we have + * per-recipient delivery status notification information. + * + * @return the number of recipients + */ + public int getRecipientDSNCount() { + return recipientDSN.length; + } + + /** + * Return the delivery status notification information for + * the specified recipient. + * + * @param n the recipient number + * @return the DSN fields for the recipient + */ + public InternetHeaders getRecipientDSN(int n) { + return recipientDSN[n]; + } + + /** + * Add deliver status notification information for another + * recipient. + * + * @param h the DSN fields for the recipient + */ + public void addRecipientDSN(InternetHeaders h) { + InternetHeaders[] rh = new InternetHeaders[recipientDSN.length + 1]; + System.arraycopy(recipientDSN, 0, rh, 0, recipientDSN.length); + recipientDSN = rh; + recipientDSN[recipientDSN.length - 1] = h; + } + + public void writeTo(OutputStream os) throws IOException { + // see if we already have a LOS + LineOutputStream los = null; + if (os instanceof LineOutputStream) { + los = (LineOutputStream) os; + } else { + los = StreamProvider.provider().outputLineStream(os, false); + } + + writeInternetHeaders(messageDSN, los); + los.writeln(); + for (int i = 0; i < recipientDSN.length; i++) { + writeInternetHeaders(recipientDSN[i], los); + los.writeln(); + } + } + + private static void writeInternetHeaders(InternetHeaders h, + LineOutputStream los) throws IOException { + Enumeration e = h.getAllHeaderLines(); + while (e.hasMoreElements()) + los.writeln(e.nextElement()); + } + + @Override + public String toString() { + return "DeliveryStatus: Reporting-MTA=" + + messageDSN.getHeader("Reporting-MTA", null) + ", #Recipients=" + + recipientDSN.length; + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/dsn/DispositionNotification.java b/net-mail/src/main/java/org/xbib/net/mail/dsn/DispositionNotification.java new file mode 100644 index 0000000..1224a20 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/dsn/DispositionNotification.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.dsn; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.InternetHeaders; +import jakarta.mail.util.LineOutputStream; +import jakarta.mail.util.StreamProvider; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Enumeration; +import java.util.logging.Logger; + +/** + * A message/disposition-notification message content, as defined in + * RFC 3798. + * + * @since JavaMail 1.4.2 + */ +public class DispositionNotification extends Report { + + private static final Logger logger = Logger.getLogger(DispositionNotification.class.getName()); + + /** + * The disposition notification content fields. + */ + protected InternetHeaders notifications; + + /** + * Construct a disposition notification with no content. + * + * @exception MessagingException for failures + */ + public DispositionNotification() throws MessagingException { + super("disposition-notification"); + notifications = new InternetHeaders(); + } + + /** + * Construct a disposition notification by parsing the + * supplied input stream. + * + * @param is the input stream + * @exception IOException for I/O errors reading the stream + * @exception MessagingException for other failures + */ + public DispositionNotification(InputStream is) + throws MessagingException, IOException { + super("disposition-notification"); + notifications = new InternetHeaders(is); + logger.fine("got MDN notification content"); + } + + /** + * Return all the disposition notification fields in the + * disposition notification. + * The fields are defined as: + * + *
+     *    disposition-notification-content =
+     * 		[ reporting-ua-field CRLF ]
+     * 		[ mdn-gateway-field CRLF ]
+     * 		[ original-recipient-field CRLF ]
+     * 		final-recipient-field CRLF
+     * 		[ original-message-id-field CRLF ]
+     * 		disposition-field CRLF
+     * 		*( failure-field CRLF )
+     * 		*( error-field CRLF )
+     * 		*( warning-field CRLF )
+     * 		*( extension-field CRLF )
+     * 
+ * + * @return the DSN fields + */ + // XXX - could parse each of these fields + public InternetHeaders getNotifications() { + return notifications; + } + + /** + * Set the disposition notification fields in the + * disposition notification. + * + * @param notifications the DSN fields + */ + public void setNotifications(InternetHeaders notifications) { + this.notifications = notifications; + } + + public void writeTo(OutputStream os) throws IOException { + // see if we already have a LOS + LineOutputStream los = null; + if (os instanceof LineOutputStream) { + los = (LineOutputStream) os; + } else { + los = StreamProvider.provider().outputLineStream(os, false); + } + + writeInternetHeaders(notifications, los); + los.writeln(); + } + + private static void writeInternetHeaders(InternetHeaders h, + LineOutputStream los) throws IOException { + Enumeration e = h.getAllHeaderLines(); + while (e.hasMoreElements()) + los.writeln(e.nextElement()); + } + + public String toString() { + return "DispositionNotification: Reporting-UA=" + + notifications.getHeader("Reporting-UA", null); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/dsn/MessageHeaders.java b/net-mail/src/main/java/org/xbib/net/mail/dsn/MessageHeaders.java new file mode 100644 index 0000000..d1d73fe --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/dsn/MessageHeaders.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.dsn; + +import jakarta.activation.DataHandler; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.InternetHeaders; +import jakarta.mail.internet.MimeMessage; +import java.io.ByteArrayInputStream; +import java.io.InputStream; + +/** + * A special MimeMessage object that contains only message headers, + * no content. Used to represent the MIME type text/rfc822-headers. + * + * @since JavaMail 1.4 + */ +public class MessageHeaders extends MimeMessage { + + /** + * Construct a MessageHeaders object. + * + * @exception MessagingException for failures + */ + public MessageHeaders() throws MessagingException { + super((Session) null); + content = new byte[0]; + } + + /** + * Constructs a MessageHeaders object from the given InputStream. + * + * @param is InputStream + * @exception MessagingException for failures + */ + public MessageHeaders(InputStream is) throws MessagingException { + super(null, is); + content = new byte[0]; + } + + /** + * Constructs a MessageHeaders object using the given InternetHeaders. + * + * @param headers InternetHeaders to use + * @exception MessagingException for failures + */ + public MessageHeaders(InternetHeaders headers) throws MessagingException { + super((Session) null); + this.headers = headers; + content = new byte[0]; + } + + /** + * Return the size of this message. + * Always returns zero. + */ + public int getSize() { + return 0; + } + + public InputStream getInputStream() { + return new ByteArrayInputStream(content); + } + + protected InputStream getContentStream() { + return new ByteArrayInputStream(content); + } + + /** + * Can't set any content for a MessageHeaders object. + * + * @exception MessagingException always + */ + public void setDataHandler(DataHandler dh) throws MessagingException { + throw new MessagingException("Can't set content for MessageHeaders"); + } + +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/dsn/MultipartReport.java b/net-mail/src/main/java/org/xbib/net/mail/dsn/MultipartReport.java new file mode 100644 index 0000000..df78ece --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/dsn/MultipartReport.java @@ -0,0 +1,446 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.dsn; + +import jakarta.activation.DataSource; +import jakarta.mail.BodyPart; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.internet.ContentType; +import jakarta.mail.internet.InternetHeaders; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import java.io.IOException; +import java.util.Vector; + +/** + * A multipart/report message content, as defined in + * RFC 3462. + * A multipart/report content is a container for mail reports + * of any kind, and is most often used to return a delivery + * status report or a disposition notification report.

+ * + * A MultipartReport object is a special type of MimeMultipart + * object with a restricted set of body parts. A MultipartReport + * object contains: + *

    + *
  • [Required] A human readable text message describing the + * reason the report was generated.
  • + *
  • [Required] A {@link Report} object containing the + * details for why the report was generated.
  • + *
  • [Optional] A returned copy of the entire message, or just + * its headers, which caused the generation of this report. + *
+ * Many of the normal MimeMultipart operations are restricted to + * ensure that the MultipartReport object always follows this + * structure. + * + * @since JavaMail 1.4 + */ +public class MultipartReport extends MimeMultipart { + protected boolean constructed; // true when done with constructor + + /** + * Construct a multipart/report object with no content. + * + * @exception MessagingException for failures + */ + @SuppressWarnings("this-escape") + public MultipartReport() throws MessagingException { + super("report"); + // always at least two body parts + MimeBodyPart mbp = new MimeBodyPart(); + setBodyPart(mbp, 0); + mbp = new MimeBodyPart(); + setBodyPart(mbp, 1); + constructed = true; + } + + /** + * Construct a multipart/report object with the specified plain + * text and report type (DeliveryStatus or DispositionNotification) + * to be returned to the user. + * + * @param text the plain text + * @param report the Report object + * @exception MessagingException for failures + */ + @SuppressWarnings("this-escape") + public MultipartReport(String text, Report report) + throws MessagingException { + super("report"); + ContentType ct = new ContentType(contentType); + String reportType = report.getType(); + ct.setParameter("report-type", reportType); + contentType = ct.toString(); + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText(text); + setBodyPart(mbp, 0); + mbp = new MimeBodyPart(); + ct = new ContentType("message", reportType, null); + mbp.setContent(report, ct.toString()); + setBodyPart(mbp, 1); + constructed = true; + } + + /** + * Construct a multipart/report object with the specified plain + * text, report, and original message to be returned to the user. + * + * @param text the plain text + * @param report the Report object + * @param msg the message this report is about + * @exception MessagingException for failures + */ + @SuppressWarnings("this-escape") + public MultipartReport(String text, Report report, MimeMessage msg) + throws MessagingException { + this(text, report); + if (msg != null) { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setContent(msg, "message/rfc822"); + setBodyPart(mbp, 2); + } + } + + /** + * Construct a multipart/report object with the specified plain + * text, report, and headers from the original message + * to be returned to the user. + * + * @param text the plain text + * @param report the Report object + * @param hdr the headers of the message this report is about + * @exception MessagingException for failures + */ + @SuppressWarnings("this-escape") + public MultipartReport(String text, Report report, InternetHeaders hdr) + throws MessagingException { + this(text, report); + if (hdr != null) { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setContent(new MessageHeaders(hdr), "text/rfc822-headers"); + setBodyPart(mbp, 2); + } + } + + /** + * Constructs a MultipartReport object and its bodyparts from the + * given DataSource. + * + * @param ds DataSource, can be a MultipartDataSource + * @exception MessagingException for failures + */ + @SuppressWarnings("this-escape") + public MultipartReport(DataSource ds) throws MessagingException { + super(ds); + parse(); + constructed = true; + /* + * Can't fail to construct object because some programs just + * want to treat this as a Multipart and examine the parts. + * + if (getCount() < 2 || getCount() > 3) // XXX allow extra parts + throw new MessagingException( + "Wrong number of parts in multipart/report: " + getCount()); + */ + } + + /** + * Get the plain text to be presented to the user, if there is any. + * Rarely, the message may contain only HTML text, or no text at + * all. If the text body part of this multipart/report object is + * of type text/plain, or if it is of type multipart/alternative + * and contains a text/plain part, the text from that part is + * returned. Otherwise, null is return and the {@link #getTextBodyPart + * getTextBodyPart} method may be used to extract the data. + * + * @return the text + * @exception MessagingException for failures + */ + public synchronized String getText() throws MessagingException { + try { + BodyPart bp = getBodyPart(0); + if (bp.isMimeType("text/plain")) + return (String) bp.getContent(); + if (bp.isMimeType("multipart/alternative")) { + Multipart mp = (Multipart) bp.getContent(); + for (int i = 0; i < mp.getCount(); i++) { + bp = mp.getBodyPart(i); + if (bp.isMimeType("text/plain")) + return (String) bp.getContent(); + } + } + } catch (IOException ex) { + throw new MessagingException("Exception getting text content", ex); + } + return null; + } + + /** + * Set the message to be presented to the user as just a text/plain + * part containing the specified text. + * + * @param text the text + * @exception MessagingException for failures + */ + public synchronized void setText(String text) throws MessagingException { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText(text); + setBodyPart(mbp, 0); + } + + /** + * Return the body part containing the message to be presented to + * the user, usually just a text/plain part. + * + * @return the body part containing the text + * @exception MessagingException for failures + */ + public synchronized MimeBodyPart getTextBodyPart() + throws MessagingException { + return (MimeBodyPart) getBodyPart(0); + } + + /** + * Set the body part containing the text to be presented to the + * user. Usually this a text/plain part, but it might also be + * a text/html part or a multipart/alternative part containing + * text/plain and text/html parts. Any type is allowed here + * but these types are most common. + * + * @param mbp the body part containing the text + * @exception MessagingException for failures + */ + public synchronized void setTextBodyPart(MimeBodyPart mbp) + throws MessagingException { + setBodyPart(mbp, 0); + } + + /** + * Get the report associated with this multipart/report. + * + * @return the Report object + * @exception MessagingException for failures + * @since JavaMail 1.4.2 + */ + public synchronized Report getReport() throws MessagingException { + if (getCount() < 2) + return null; + BodyPart bp = getBodyPart(1); + try { + Object content = bp.getContent(); + if (!(content instanceof Report)) + return null; + return (Report) content; + } catch (IOException ex) { + throw new MessagingException("IOException getting Report", ex); + } + } + + /** + * Set the report associated with this multipart/report. + * + * @param report the Report object + * @exception MessagingException for failures + * @since JavaMail 1.4.2 + */ + public synchronized void setReport(Report report) + throws MessagingException { + MimeBodyPart mbp = new MimeBodyPart(); + ContentType ct = new ContentType(contentType); + String reportType = report.getType(); + ct.setParameter("report-type", reportType); + contentType = ct.toString(); + ct = new ContentType("message", reportType, null); + mbp.setContent(report, ct.toString()); + setBodyPart(mbp, 1); + } + + /** + * Get the delivery status associated with this multipart/report. + * + * @return the delivery status + * @exception MessagingException for failures + * @deprecated use getReport instead + */ + @Deprecated + public synchronized DeliveryStatus getDeliveryStatus() + throws MessagingException { + if (getCount() < 2) + return null; + BodyPart bp = getBodyPart(1); + if (!bp.isMimeType("message/delivery-status")) + return null; + try { + return (DeliveryStatus) bp.getContent(); + } catch (IOException ex) { + throw new MessagingException("IOException getting DeliveryStatus", + ex); + } + } + + /** + * Set the delivery status associated with this multipart/report. + * + * @param status the deliver status + * @exception MessagingException for failures + * @deprecated use setReport instead + */ + @Deprecated + public synchronized void setDeliveryStatus(DeliveryStatus status) + throws MessagingException { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setContent(status, "message/delivery-status"); + setBodyPart(mbp, 1); + ContentType ct = new ContentType(contentType); + ct.setParameter("report-type", "delivery-status"); + contentType = ct.toString(); + } + + /** + * Get the original message that is being returned along with this + * multipart/report. If no original message is included, null is + * returned. In some cases only the headers of the original + * message will be returned as an object of type MessageHeaders. + * + * @return the returned message + * @exception MessagingException for failures + */ + public synchronized MimeMessage getReturnedMessage() + throws MessagingException { + if (getCount() < 3) + return null; + BodyPart bp = getBodyPart(2); + if (!bp.isMimeType("message/rfc822") && + !bp.isMimeType("text/rfc822-headers")) + return null; + try { + return (MimeMessage) bp.getContent(); + } catch (IOException ex) { + throw new MessagingException("IOException getting ReturnedMessage", + ex); + } + } + + /** + * Set the original message to be returned as part of the + * multipart/report. If msg is null, any previously set + * returned message or headers is removed. + * + * @param msg the returned message + * @exception MessagingException for failures + */ + public synchronized void setReturnedMessage(MimeMessage msg) + throws MessagingException { + if (msg == null) { + super.removeBodyPart(2); + return; + } + MimeBodyPart mbp = new MimeBodyPart(); + if (msg instanceof MessageHeaders) + mbp.setContent(msg, "text/rfc822-headers"); + else + mbp.setContent(msg, "message/rfc822"); + setBodyPart(mbp, 2); + } + + private synchronized void setBodyPart(BodyPart part, int index) + throws MessagingException { + if (parts == null) // XXX - can never happen? + parts = new Vector<>(); + + if (index < parts.size()) + super.removeBodyPart(index); + super.addBodyPart(part, index); + } + + + // Override Multipart methods to preserve integrity of multipart/report. + + /** + * Set the subtype. Throws MessagingException. + * + * @param subtype Subtype + * @exception MessagingException always; can't change subtype + */ + @Override + public synchronized void setSubType(String subtype) + throws MessagingException { + throw new MessagingException("Can't change subtype of MultipartReport"); + } + + /** + * Remove the specified part from the multipart message. + * Not allowed on a multipart/report object. + * + * @param part The part to remove + * @exception MessagingException always + */ + @Override + public boolean removeBodyPart(BodyPart part) throws MessagingException { + throw new MessagingException( + "Can't remove body parts from multipart/report"); + } + + /** + * Remove the part at specified location (starting from 0). + * Not allowed on a multipart/report object. + * + * @param index Index of the part to remove + * @exception MessagingException always + */ + @Override + public void removeBodyPart(int index) throws MessagingException { + throw new MessagingException( + "Can't remove body parts from multipart/report"); + } + + /** + * Adds a Part to the multipart. + * Not allowed on a multipart/report object. + * + * @param part The Part to be appended + * @throws MessagingException always + */ + @Override + public synchronized void addBodyPart(BodyPart part) + throws MessagingException { + // Once constructor is done, don't allow this anymore. + if (!constructed) + super.addBodyPart(part); + else + throw new MessagingException( + "Can't add body parts to multipart/report 1"); + } + + /** + * Adds a BodyPart at position index. + * Not allowed on a multipart/report object. + * + * @param part The BodyPart to be inserted + * @param index Location where to insert the part + * @throws MessagingException always + */ + @Override + public synchronized void addBodyPart(BodyPart part, int index) + throws MessagingException { + throw new MessagingException( + "Can't add body parts to multipart/report 2"); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/dsn/Report.java b/net-mail/src/main/java/org/xbib/net/mail/dsn/Report.java new file mode 100644 index 0000000..9e2eee8 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/dsn/Report.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.dsn; + +/** + * An abstract report type, to be included in a MultipartReport. + * Subclasses define specific report types, such as {@link DeliveryStatus} + * and {@link DispositionNotification}. + * + * @since JavaMail 1.4.2 + */ +public abstract class Report { + protected String type; // the MIME subtype of the report + + /** + * Construct a report of the indicated MIME subtype. + * The primary MIME type is always "message". + * + * @param type the MIME subtype + */ + protected Report(String type) { + this.type = type; + } + + /** + * Get the MIME subtype of the report. + * The primary MIME type is always "message". + * + * @return the MIME subtype + */ + public String getType() { + return type; + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/dsn/message_deliverystatus.java b/net-mail/src/main/java/org/xbib/net/mail/dsn/message_deliverystatus.java new file mode 100644 index 0000000..f5b98b9 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/dsn/message_deliverystatus.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.dsn; + +import jakarta.activation.ActivationDataFlavor; +import jakarta.activation.DataContentHandler; +import jakarta.activation.DataSource; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.io.OutputStream; +//import jakarta.mail.internet.*; + + +/** + * DataContentHandler for message/delivery-status MIME type. + * Applications should not use this class directly, it's used indirectly + * through the JavaBeans Activation Framework. + * + * @since JavaMail 1.4 + */ +public class message_deliverystatus implements DataContentHandler { + + ActivationDataFlavor ourDataFlavor = new ActivationDataFlavor( + DeliveryStatus.class, + "message/delivery-status", + "Delivery Status"); + + /** + * Creates a default {@code message_deliverystatus}. + */ + public message_deliverystatus() { + } + + /** + * return the ActivationDataFlavors for this DataContentHandler + * + * @return The ActivationDataFlavors. + */ + public ActivationDataFlavor[] getTransferDataFlavors() { + return new ActivationDataFlavor[]{ourDataFlavor}; + } + + /** + * return the Transfer Data of type ActivationDataFlavor from InputStream + * + * @param df The ActivationDataFlavor. + * @param ds The DataSource corresponding to the data. + * @return a Message object + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws IOException { + // make sure we can handle this ActivationDataFlavor + if (ourDataFlavor.equals(df)) + return getContent(ds); + else + return null; + } + + /** + * Return the content. + */ + public Object getContent(DataSource ds) throws IOException { + // create a new DeliveryStatus + try { + /* + Session session; + if (ds instanceof MessageAware) { + jakarta.mail.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 DeliveryStatus(session, ds.getInputStream()); + */ + return new DeliveryStatus(ds.getInputStream()); + } catch (MessagingException me) { + throw new IOException("Exception creating DeliveryStatus in " + + "message/delivery-status DataContentHandler: " + + me.toString()); + } + } + + /** + * + */ + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + // if the object is a DeliveryStatus, we know how to write that out + if (obj instanceof DeliveryStatus) { + DeliveryStatus ds = (DeliveryStatus) obj; + ds.writeTo(os); + + } else { + throw new IOException("unsupported object"); + } + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/dsn/message_dispositionnotification.java b/net-mail/src/main/java/org/xbib/net/mail/dsn/message_dispositionnotification.java new file mode 100644 index 0000000..02f7534 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/dsn/message_dispositionnotification.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.dsn; + +import jakarta.activation.ActivationDataFlavor; +import jakarta.activation.DataContentHandler; +import jakarta.activation.DataSource; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.io.OutputStream; +//import jakarta.mail.internet.*; + + +/** + * DataContentHandler for message/disposition-notification MIME type. + * Applications should not use this class directly, it's used indirectly + * through the JavaBeans Activation Framework. + * + * @since JavaMail 1.4.2 + */ +public class message_dispositionnotification implements DataContentHandler { + + ActivationDataFlavor ourDataFlavor = new ActivationDataFlavor( + DispositionNotification.class, + "message/disposition-notification", + "Disposition Notification"); + + /** + * Creates a default {@code message_dispositionnotification}. + */ + public message_dispositionnotification() { + } + + /** + * return the ActivationDataFlavors for this DataContentHandler + * + * @return The ActivationDataFlavors. + */ + public ActivationDataFlavor[] getTransferDataFlavors() { + return new ActivationDataFlavor[]{ourDataFlavor}; + } + + /** + * return the Transfer Data of type ActivationDataFlavor from InputStream + * + * @param df The ActivationDataFlavor. + * @param ds The DataSource corresponding to the data. + * @return a Message object + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws IOException { + // make sure we can handle this ActivationDataFlavor + if (ourDataFlavor.equals(df)) + return getContent(ds); + else + return null; + } + + /** + * Return the content. + */ + public Object getContent(DataSource ds) throws IOException { + // create a new DispositionNotification + try { + /* + Session session; + if (ds instanceof MessageAware) { + jakarta.mail.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 DispositionNotification(session, ds.getInputStream()); + */ + return new DispositionNotification(ds.getInputStream()); + } catch (MessagingException me) { + throw new IOException( + "Exception creating DispositionNotification in " + + "message/disposition-notification DataContentHandler: " + + me.toString()); + } + } + + /** + * + */ + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + // if it's a DispositionNotification, we know how to write that out + if (obj instanceof DispositionNotification) { + DispositionNotification dn = (DispositionNotification) obj; + dn.writeTo(os); + } else { + throw new IOException("unsupported object"); + } + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/dsn/multipart_report.java b/net-mail/src/main/java/org/xbib/net/mail/dsn/multipart_report.java new file mode 100644 index 0000000..e396cfa --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/dsn/multipart_report.java @@ -0,0 +1,99 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.dsn; + +import jakarta.activation.ActivationDataFlavor; +import jakarta.activation.DataContentHandler; +import jakarta.activation.DataSource; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.io.OutputStream; + + +/** + * DataContentHandler for multipart/report MIME type. + * Applications should not use this class directly, it's used indirectly + * through the JavaBeans Activation Framework. + * + * @since JavaMail 1.4 + */ +public class multipart_report implements DataContentHandler { + private ActivationDataFlavor myDF = new ActivationDataFlavor( + MultipartReport.class, + "multipart/report", + "Multipart Report"); + + /** + * Creates a default {@code multipart_report}. + */ + public multipart_report() { + } + + /** + * Return the ActivationDataFlavors for this DataContentHandler. + * + * @return The ActivationDataFlavors + */ + public ActivationDataFlavor[] getTransferDataFlavors() { // throws Exception; + return new ActivationDataFlavor[]{myDF}; + } + + /** + * Return the Transfer Data of type ActivationDataFlavor from InputStream. + * + * @param df The ActivationDataFlavor + * @param ds The DataSource corresponding to the data + * @return String object + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws IOException { + // use myDF.equals to be sure to get ActivationDataFlavor.equals, + // which properly ignores Content-Type parameters in comparison + if (myDF.equals(df)) + return getContent(ds); + else + return null; + } + + /** + * Return the content. + */ + public Object getContent(DataSource ds) throws IOException { + try { + return new MultipartReport(ds); + } catch (MessagingException e) { + IOException ioex = + new IOException("Exception while constructing MultipartReport"); + ioex.initCause(e); + throw ioex; + } + } + + /** + * Write the object to the output stream, using the specific MIME type. + */ + public void writeTo(Object obj, String mimeType, OutputStream os) + throws IOException { + if (obj instanceof MultipartReport) { + try { + ((MultipartReport) obj).writeTo(os); + } catch (MessagingException e) { + throw new IOException(e.toString()); + } + } + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/dsn/package-info.java b/net-mail/src/main/java/org/xbib/net/mail/dsn/package-info.java new file mode 100644 index 0000000..502bd84 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/dsn/package-info.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * Support for creating and parsing Delivery Status Notifications. + * Refer to + * RFC 3462 + * and RFC 3464 + * for more information. + *
+ *

+ * A Delivery Status Notification is a MIME message with a Content-Type + * of multipart/report. + * A {@link org.xbib.net.mail.dsn.MultipartReport MultipartReport} object + * represents the content of such a message. + * The MultipartReport object contains several parts that represent the + * information in a delivery status notification. + * The first part is usually a text/plain part that + * describes the reason for the notification. + * The second part is a message/delivery-status part, + * which is represented by a + * {@link org.xbib.net.mail.dsn.DeliveryStatus DeliveryStatus} object, and contains + * details about the notification. + * The third part is either an entire copy of the original message + * that is returned, represented by a + * {@link jakarta.mail.internet.MimeMessage MimeMessage} object, or + * just the headers of the original message, represented by a + * {@link org.xbib.net.mail.dsn.MessageHeaders MessageHeaders} object. + *

+ *

+ * To use the classes in this package, include dsn.jar + * in your class path. + *

+ *

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

+ * + * + * + * + * + * + * + * + * + * + * + * + * + *
org.xbib.net.mail.dsn Loggers
Logger NameLogging LevelPurpose
org.xbib.net.mail.dsnFINERGeneral debugging output
+ * + *

+ * 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. + */ +package org.xbib.net.mail.dsn; diff --git a/net-mail/src/main/java/org/xbib/net/mail/dsn/text_rfc822headers.java b/net-mail/src/main/java/org/xbib/net/mail/dsn/text_rfc822headers.java new file mode 100644 index 0000000..49f18f0 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/dsn/text_rfc822headers.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.dsn; + +import jakarta.activation.ActivationDataFlavor; +import jakarta.activation.DataContentHandler; +import jakarta.activation.DataSource; +import jakarta.mail.MessagingException; +import jakarta.mail.internet.ContentType; +import jakarta.mail.internet.MimeUtility; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; + +/** + * DataContentHandler for text/rfc822-headers MIME type. + * Applications should not use this class directly, it's used indirectly + * through the JavaBeans Activation Framework. + * + * @since JavaMail 1.4 + */ +public class text_rfc822headers implements DataContentHandler { + private static final ActivationDataFlavor myDF = new ActivationDataFlavor( + MessageHeaders.class, + "text/rfc822-headers", + "RFC822 headers"); + private static final ActivationDataFlavor myDFs = new ActivationDataFlavor( + String.class, + "text/rfc822-headers", + "RFC822 headers"); + + /** + * Creates a default {@code text_rfc822headers}. + */ + public text_rfc822headers() { + } + + /** + * Return the ActivationDataFlavors for this DataContentHandler. + * + * @return The ActivationDataFlavors + */ + public ActivationDataFlavor[] getTransferDataFlavors() { + return new ActivationDataFlavor[]{myDF, myDFs}; + } + + /** + * Return the Transfer Data of type ActivationDataFlavor from InputStream. + * + * @param df The ActivationDataFlavor + * @param ds The DataSource corresponding to the data + * @return String object + */ + public Object getTransferData(ActivationDataFlavor df, DataSource ds) + throws IOException { + // use myDF.equals to be sure to get ActivationDataFlavor.equals, + // which properly ignores Content-Type parameters in comparison + if (myDF.equals(df)) + return getContent(ds); + else if (myDFs.equals(df)) + return getStringContent(ds); + else + return null; + } + + public Object getContent(DataSource ds) throws IOException { + try { + return new MessageHeaders(ds.getInputStream()); + } catch (MessagingException mex) { + throw new IOException("Exception creating MessageHeaders: " + mex); + } + } + + private Object getStringContent(DataSource ds) throws IOException { + String enc = null; + InputStreamReader is; + 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; + size += Math.min(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. + */ + public void writeTo(Object obj, String type, OutputStream os) + throws IOException { + if (obj instanceof MessageHeaders) { + MessageHeaders mh = (MessageHeaders) obj; + try { + mh.writeTo(os); + } catch (MessagingException mex) { + Exception ex = mex.getNextException(); + if (ex instanceof IOException) + throw (IOException) ex; + else + throw new IOException("Exception writing headers: " + mex); + } + return; + } + if (!(obj instanceof String)) + throw new IOException("\"" + myDFs.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(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()); + osw.flush(); + } + + 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/net-mail/src/main/java/org/xbib/net/mail/handlers/handler_base.java b/net-mail/src/main/java/org/xbib/net/mail/handlers/handler_base.java new file mode 100644 index 0000000..635bb9a --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/handlers/handler_base.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.handlers; + +import jakarta.activation.ActivationDataFlavor; +import jakarta.activation.DataContentHandler; +import jakarta.activation.DataSource; +import java.io.IOException; + +/** + * Base class for other DataContentHandlers. + */ +public abstract class handler_base implements DataContentHandler { + + /** + * Creates a default {@code handler_base}. + */ + public handler_base() { + } + + /** + * 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 + * @throws IOException for errors reading the data + */ + protected Object getData(ActivationDataFlavor aFlavor, DataSource ds) + throws IOException { + return getContent(ds); + } + + /** + * Return the ActivationDataFlavors for this DataContentHandler. + * + * @return The ActivationDataFlavors + */ + @Override + public ActivationDataFlavor[] getTransferDataFlavors() { + ActivationDataFlavor[] adf = getDataFlavors(); + if (adf.length == 1) // the common case + return new ActivationDataFlavor[]{adf[0]}; + ActivationDataFlavor[] df = new ActivationDataFlavor[adf.length]; + System.arraycopy(adf, 0, df, 0, adf.length); + return df; + } + + /** + * Return the Transfer Data of type ActivationDataFlavor from InputStream. + * + * @param df The ActivationDataFlavor + * @param ds The DataSource corresponding to the data + * @return the object + * @throws IOException for errors reading the data + */ + @Override + 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/net-mail/src/main/java/org/xbib/net/mail/handlers/message_rfc822.java b/net-mail/src/main/java/org/xbib/net/mail/handlers/message_rfc822.java new file mode 100644 index 0000000..f5f27ac --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/handlers/message_rfc822.java @@ -0,0 +1,105 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.handlers; + +import jakarta.activation.ActivationDataFlavor; +import jakarta.activation.DataSource; +import jakarta.mail.Message; +import jakarta.mail.MessageAware; +import jakarta.mail.MessageContext; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Properties; + + +/** + * @author Christopher Cotton + */ + + +public class message_rfc822 extends handler_base { + + private static ActivationDataFlavor[] ourDataFlavor = { + new ActivationDataFlavor(Message.class, "message/rfc822", "Message") + }; + + /** + * Creates a default {@code message_rfc822}. + */ + public message_rfc822() { + } + + @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/net-mail/src/main/java/org/xbib/net/mail/handlers/multipart_mixed.java b/net-mail/src/main/java/org/xbib/net/mail/handlers/multipart_mixed.java new file mode 100644 index 0000000..cdcbc65 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/handlers/multipart_mixed.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.handlers; + +import jakarta.activation.ActivationDataFlavor; +import jakarta.activation.DataSource; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.internet.MimeMultipart; +import java.io.IOException; +import java.io.OutputStream; + + +public class multipart_mixed extends handler_base { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(Multipart.class, + "multipart/mixed", "Multipart") + }; + + /** + * Creates a default {@code multipart_mixed}. + */ + public multipart_mixed() { + } + + @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/net-mail/src/main/java/org/xbib/net/mail/handlers/package-info.java b/net-mail/src/main/java/org/xbib/net/mail/handlers/package-info.java new file mode 100644 index 0000000..6df160b --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/handlers/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * This package includes internal data handler support classes and + * SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. + */ +package org.xbib.net.mail.handlers; diff --git a/net-mail/src/main/java/org/xbib/net/mail/handlers/text_html.java b/net-mail/src/main/java/org/xbib/net/mail/handlers/text_html.java new file mode 100644 index 0000000..ffabab7 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/handlers/text_html.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.handlers; + +import jakarta.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") + }; + + /** + * Creates a default {@code text_html}. + */ + public text_html() { + } + + @Override + protected ActivationDataFlavor[] getDataFlavors() { + return myDF; + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/handlers/text_plain.java b/net-mail/src/main/java/org/xbib/net/mail/handlers/text_plain.java new file mode 100644 index 0000000..3701be0 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/handlers/text_plain.java @@ -0,0 +1,161 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.handlers; + +import jakarta.activation.ActivationDataFlavor; +import jakarta.activation.DataSource; +import jakarta.mail.internet.ContentType; +import jakarta.mail.internet.MimeUtility; +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.UnsupportedEncodingException; + +/** + * DataContentHandler for text/plain. + */ +public class text_plain extends handler_base { + private static ActivationDataFlavor[] myDF = { + new ActivationDataFlavor(String.class, "text/plain", "Text String") + }; + + /** + * Creates a default {@code text_plain}. + */ + public text_plain() { + } + + @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; + size += Math.min(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; + } + } + + /** + * An OuputStream wrapper that doesn't close the underlying stream. + */ + private static class NoCloseOutputStream extends FilterOutputStream { + NoCloseOutputStream(OutputStream os) { + super(os); + } + + @Override + public void close() { + // do nothing + } + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/handlers/text_xml.java b/net-mail/src/main/java/org/xbib/net/mail/handlers/text_xml.java new file mode 100644 index 0000000..0b84e5b --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/handlers/text_xml.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.handlers; + +import jakarta.activation.ActivationDataFlavor; +import jakarta.activation.DataSource; +import jakarta.mail.internet.ContentType; +import jakarta.mail.internet.ParseException; +import java.io.IOException; +import java.io.OutputStream; +import javax.xml.transform.Source; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +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") + }; + + /** + * Creates a default {@code text_xml}. + */ + public text_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 | 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 | RuntimeException ex) { + return false; + } + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/iap/Argument.java b/net-mail/src/main/java/org/xbib/net/mail/iap/Argument.java new file mode 100644 index 0000000..5c0eb70 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/Argument.java @@ -0,0 +1,440 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.iap; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import org.xbib.net.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/net-mail/src/main/java/org/xbib/net/mail/iap/BadCommandException.java b/net-mail/src/main/java/org/xbib/net/mail/iap/BadCommandException.java new file mode 100644 index 0000000..4defa2a --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/BadCommandException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.iap; + +/** + * @author John Mani + */ +@SuppressWarnings("serial") +public class BadCommandException extends ProtocolException { + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/iap/ByteArray.java b/net-mail/src/main/java/org/xbib/net/mail/iap/ByteArray.java new file mode 100644 index 0000000..e003524 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/ByteArray.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.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/net-mail/src/main/java/org/xbib/net/mail/iap/CommandFailedException.java b/net-mail/src/main/java/org/xbib/net/mail/iap/CommandFailedException.java new file mode 100644 index 0000000..892412f --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/CommandFailedException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.iap; + +/** + * @author John Mani + */ +@SuppressWarnings("serial") +public class CommandFailedException extends ProtocolException { + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/iap/ConnectionException.java b/net-mail/src/main/java/org/xbib/net/mail/iap/ConnectionException.java new file mode 100644 index 0000000..3e6dac9 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/ConnectionException.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.iap; + +/** + * @author John Mani + */ +@SuppressWarnings("serial") +public class ConnectionException extends ProtocolException { + private transient Protocol p; + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/iap/Literal.java b/net-mail/src/main/java/org/xbib/net/mail/iap/Literal.java new file mode 100644 index 0000000..883f8da --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/Literal.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.iap; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/iap/LiteralException.java b/net-mail/src/main/java/org/xbib/net/mail/iap/LiteralException.java new file mode 100644 index 0000000..daad771 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/LiteralException.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.iap; + +/** + * @author Bill Shannon + */ +@SuppressWarnings("serial") +public class LiteralException extends ProtocolException { + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/iap/ParsingException.java b/net-mail/src/main/java/org/xbib/net/mail/iap/ParsingException.java new file mode 100644 index 0000000..af199c6 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/ParsingException.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.iap; + +/** + * @author John Mani + */ +@SuppressWarnings("serial") +public class ParsingException extends ProtocolException { + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/iap/Protocol.java b/net-mail/src/main/java/org/xbib/net/mail/iap/Protocol.java new file mode 100644 index 0000000..762d49e --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/Protocol.java @@ -0,0 +1,695 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.iap; + +import java.io.BufferedOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.reflect.Field; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.UnknownHostException; +import java.nio.channels.SocketChannel; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.zip.Deflater; +import java.util.zip.DeflaterOutputStream; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; +import javax.net.ssl.SSLSocket; +import org.xbib.net.mail.util.PropUtil; +import org.xbib.net.mail.util.SocketFetcher; +import org.xbib.net.mail.util.TraceInputStream; +import org.xbib.net.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 { + + private static final Logger logger = Logger.getLogger(Protocol.class.getName()); + + protected String host; + private Socket socket; + // in case we turn on TLS, we'll need these later + protected boolean quote; + 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; + + public 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? + * @exception IOException for I/O errors + * @exception ProtocolException for protocol failures + */ + @SuppressWarnings("this-escape") + public Protocol(String host, int port, + Properties props, String prefix, + boolean isSSL) + throws IOException, ProtocolException { + boolean connected = false; // did constructor succeed? + tagPrefix = computePrefix(props, prefix); + try { + this.host = host; + this.props = props; + this.prefix = prefix; + + 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()); + traceInput.setQuote(quote); + input = new ResponseInputStream(traceInput); + traceOutput = new TraceOutputStream(socket.getOutputStream()); + 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 = String.valueOf((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"); + + // XXX - inlined initStreams, won't allow later startTLS + traceInput = new TraceInputStream(in); + traceInput.setQuote(quote); + input = new ResponseInputStream(traceInput); + traceOutput = new TraceOutputStream(out); + 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)); + 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, MessageFormat.format( + "Creating Deflater with compression level {0} and strategy {1}", + 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)); + 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() { + //SocketFetcher controls if a socket has a channel via + //usesocketchannels property. When the socket is known to not have + //a channel this guard ensures that the reflective search for a socket + //channel is avoided which can print warnings to error stream. + //This is assuming the session properties are not mutated after the + //socket has been connected. + if (PropUtil.getBooleanProperty(props, + prefix + ".usesocketchannels", false)) { + SocketChannel ret = socket.getChannel(); + if (ret == null && socket instanceof SSLSocket) { + ret = Protocol.findSocketChannel(socket); + } + return ret; + } + return null; + } + + /** + * Android/Conscrypt is broken and SSL wrapped sockets don't delegate + * the getChannel method to the wrapped Socket. This method attempts to + * examine the internals of the SSLSocket to locate the transport socket. + * + * @param socket a non null socket + * @return the SocketChannel or null if not found + * @throws NullPointerException if given socket is null + */ + private static SocketChannel findSocketChannel(Socket socket) { + //Search class hierarchy for field name socket regardless of modifier. + //Old versions of Android and even versions of Conscrypt use this name. + for (Class k = socket.getClass(); k != Object.class; k = k.getSuperclass()) { + try { + Field f = k.getDeclaredField("socket"); + f.setAccessible(true); + Socket s = (Socket) f.get(socket); + if (s != socket) { //reference compare only + SocketChannel ret = s.getChannel(); + if (ret != null) { + return ret; + } + } + } catch (Exception ignore) { + //ignore anything that might go wrong + } + } + + //Search class hierarchy for fields that can hold a Socket + //or subclass regardless of modifier but ignoring synthetic fields. + //Fields declared as super types of Socket be ignored. + for (Class k = socket.getClass(); k != Object.class; k = k.getSuperclass()) { + try { + for (Field f : k.getDeclaredFields()) { + if (Socket.class.isAssignableFrom(f.getType()) + && !f.isSynthetic()) { + try { + f.setAccessible(true); + Socket s = (Socket) f.get(socket); + if (s != socket) { //reference compare only + SocketChannel ret = s.getChannel(); + if (ret != null) { + return ret; + } + } + } catch (Exception ignore) { + //ignore anything that might go wrong + } + } + } + } catch (Exception ignore) { + //ignore anything that might go wrong + } + } + return null; + } + + /** + * 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; + } + + /* + * Probe points for GlassFish monitoring. + */ + private void commandStart(String command) { + } + + private void commandEnd() { + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/iap/ProtocolException.java b/net-mail/src/main/java/org/xbib/net/mail/iap/ProtocolException.java new file mode 100644 index 0000000..990a5fc --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/ProtocolException.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.iap; + +/** + * @author John Mani + */ +@SuppressWarnings("serial") +public class ProtocolException extends Exception { + protected transient Response response = null; + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/iap/Response.java b/net-mail/src/main/java/org/xbib/net/mail/iap/Response.java new file mode 100644 index 0000000..1c9fdef --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/Response.java @@ -0,0 +1,606 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.iap; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import org.xbib.net.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 + */ + @SuppressWarnings("this-escape") + 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 + */ + @SuppressWarnings("this-escape") + 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; + } + + 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[0]); + } + + /** + * 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; + 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/net-mail/src/main/java/org/xbib/net/mail/iap/ResponseHandler.java b/net-mail/src/main/java/org/xbib/net/mail/iap/ResponseHandler.java new file mode 100644 index 0000000..b3bc5ed --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/ResponseHandler.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.iap; + +/** + * This class + * + * @author John Mani + */ + +public interface ResponseHandler { + public void handleResponse(Response r); +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/iap/ResponseInputStream.java b/net-mail/src/main/java/org/xbib/net/mail/iap/ResponseInputStream.java new file mode 100644 index 0000000..5c89e34 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/ResponseInputStream.java @@ -0,0 +1,157 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.iap; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import org.xbib.net.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(Math.max(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/net-mail/src/main/java/org/xbib/net/mail/iap/package-info.java b/net-mail/src/main/java/org/xbib/net/mail/iap/package-info.java new file mode 100644 index 0000000..680da71 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/iap/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * This package includes internal IMAP support classes and + * SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. + */ +package org.xbib.net.mail.iap; diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/ACL.java b/net-mail/src/main/java/org/xbib/net/mail/imap/ACL.java new file mode 100644 index 0000000..d5ed540 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/ACL.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +/** + * 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; + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/AppendUID.java b/net-mail/src/main/java/org/xbib/net/mail/imap/AppendUID.java new file mode 100644 index 0000000..b3c3d39 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/AppendUID.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/CopyUID.java b/net-mail/src/main/java/org/xbib/net/mail/imap/CopyUID.java new file mode 100644 index 0000000..b883c9b --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/CopyUID.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import org.xbib.net.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/net-mail/src/main/java/org/xbib/net/mail/imap/DefaultFolder.java b/net-mail/src/main/java/org/xbib/net/mail/imap/DefaultFolder.java new file mode 100644 index 0000000..8f3bfd8 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/DefaultFolder.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.MethodNotSupportedException; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.imap.protocol.IMAPProtocol; +import org.xbib.net.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/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPBodyPart.java b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPBodyPart.java new file mode 100644 index 0000000..7748c04 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPBodyPart.java @@ -0,0 +1,467 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.activation.DataHandler; +import jakarta.mail.FolderClosedException; +import jakarta.mail.Header; +import jakarta.mail.IllegalWriteException; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.internet.ContentType; +import jakarta.mail.internet.InternetHeaders; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeUtility; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.SequenceInputStream; +import java.io.UnsupportedEncodingException; +import java.util.Enumeration; +import org.xbib.net.mail.iap.ConnectionException; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.imap.protocol.BODY; +import org.xbib.net.mail.imap.protocol.BODYSTRUCTURE; +import org.xbib.net.mail.imap.protocol.IMAPProtocol; +import org.xbib.net.mail.util.LineOutputStream; +import org.xbib.net.mail.util.PropUtil; +import org.xbib.net.mail.util.ReadableMime; +import org.xbib.net.mail.util.SharedByteArrayOutputStream; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPFolder.java b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPFolder.java new file mode 100644 index 0000000..d09dfab --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPFolder.java @@ -0,0 +1,4166 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.FetchProfile; +import jakarta.mail.Flags; +import jakarta.mail.Folder; +import jakarta.mail.FolderClosedException; +import jakarta.mail.FolderNotFoundException; +import jakarta.mail.Message; +import jakarta.mail.MessageRemovedException; +import jakarta.mail.MessagingException; +import jakarta.mail.Quota; +import jakarta.mail.ReadOnlyFolderException; +import jakarta.mail.StoreClosedException; +import jakarta.mail.UIDFolder; +import jakarta.mail.event.ConnectionEvent; +import jakarta.mail.event.FolderEvent; +import jakarta.mail.event.MailEvent; +import jakarta.mail.event.MessageChangedEvent; +import jakarta.mail.event.MessageCountListener; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.search.FlagTerm; +import jakarta.mail.search.SearchException; +import jakarta.mail.search.SearchTerm; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.net.SocketTimeoutException; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Date; +import java.util.Hashtable; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.mail.iap.BadCommandException; +import org.xbib.net.mail.iap.CommandFailedException; +import org.xbib.net.mail.iap.ConnectionException; +import org.xbib.net.mail.iap.Literal; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.iap.Response; +import org.xbib.net.mail.iap.ResponseHandler; +import org.xbib.net.mail.imap.protocol.FLAGS; +import org.xbib.net.mail.imap.protocol.FetchItem; +import org.xbib.net.mail.imap.protocol.FetchResponse; +import org.xbib.net.mail.imap.protocol.IMAPProtocol; +import org.xbib.net.mail.imap.protocol.IMAPResponse; +import org.xbib.net.mail.imap.protocol.Item; +import org.xbib.net.mail.imap.protocol.ListInfo; +import org.xbib.net.mail.imap.protocol.MODSEQ; +import org.xbib.net.mail.imap.protocol.MailboxInfo; +import org.xbib.net.mail.imap.protocol.MessageSet; +import org.xbib.net.mail.imap.protocol.Status; +import org.xbib.net.mail.imap.protocol.UID; +import org.xbib.net.mail.imap.protocol.UIDSet; +import org.xbib.net.mail.util.CRLFOutputStream; + +/** + * 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, ResyncData) open(int,ResyncData)} + * method and {@link 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 ProtocolCommand IMAPFolder.ProtocolCommand} + * interface support use of arbitrary IMAP protocol commands.

+ * + * See the org.xbib.net.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 { + + private static final Logger logger = Logger.getLogger(IMAPFolder.class.getName()); + + 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 + + /** + * 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"); + + /** + * 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; + + /* + * 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. + * + * @throws MessagingException if the open fails + * @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 + * @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(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(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(FetchProfileItem.SIZE)) { + command.append(first ? "RFC822.SIZE" : " RFC822.SIZE"); + first = false; + } + if (fp.contains(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().contains("TRYCREATE")) + 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().contains("TRYCREATE")) + 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[0]); + } + } 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 + * @since JavaMail 1.6.0 + * @see "RFC 4315" + */ + 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 + * @since JavaMail 1.5.1 + * @see "RFC 4551" + */ + 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 + * @since JavaMail 1.5.1 + * @see "RFC 4551" + */ + 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. + * + * @throws MessagingException if the server doesn't support the + * QUOTA extension + * @return array of Quota objects for the quotaroots associated with + * this folder + */ + 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. + * + * @throws MessagingException if the server doesn't support the + * QUOTA extension + * @param quota the quota to set + */ + 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. + * + * @throws MessagingException if the server doesn't support the + * ACL extension + * @return array of access control list entries + */ + 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. + * + * @throws MessagingException if the server doesn't support the + * ACL extension + * @param acl the access control list entry to add + */ + 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. + * + * @throws MessagingException if the server doesn't support the + * ACL extension + * @param name the identifier for which to remove all ACL entries + */ + 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. + * + * @throws MessagingException if the server doesn't support the + * ACL extension + * @param acl the identifer and rights to add + */ + 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. + * + * @throws MessagingException if the server doesn't support the + * ACL extension + * @param acl the identifer and rights to remove + */ + 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. + * + * @throws MessagingException if the server doesn't support the + * ACL extension + * @param name the identifier to list rights for + * @return array of Rights objects representing possible + * rights for the identifier + */ + 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. + * + * @throws MessagingException if the server doesn't support the + * ACL extension + * @return the rights granted to the current user + */ + 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. + * + * @throws MessagingException if the server doesn't support the + * IDLE extension + * @throws 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. + * + * @throws MessagingException if the server doesn't support the + * IDLE extension + * @throws IllegalStateException if the folder isn't open + * @param once only do one notification? + * @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. + * + * @throws MessagingException if the server doesn't support the + * IDLE extension + * @throws IllegalStateException if the folder isn't open + * @return true if IDLE started, false otherwise + * @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). + * + * @throws MessagingException for errors + * @param once only do one notification? + * @return true if we should look for more IDLE responses, + * false if IDLE is done + * @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. + * + * @throws MessagingException if the server doesn't support the + * ID extension + * @param clientParams a Map of keys and values identifying the client + * @return a Map of keys and values identifying the server + * @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. + * + * @throws MessagingException for errors + * @param item the STATUS item to fetch + * @return the value of the STATUS item, or -1 + * @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 -= (int)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 { + logger.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 org.xbib.net.mail.iap and + * org.xbib.net.mail.imap.protocol packages, best acquired by + * reading the source code. + * + *

+     * import org.xbib.net.mail.iap.*;
+     * import org.xbib.net.mail.imap.*;
+     * import org.xbib.net.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 org.xbib.net.mail.iap.*;
+     * import org.xbib.net.mail.imap.*;
+     * import org.xbib.net.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)
+     * 			   logger.log(Level.INFO, 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/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPInputStream.java b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPInputStream.java new file mode 100644 index 0000000..6cfd80d --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPInputStream.java @@ -0,0 +1,301 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Flags; +import jakarta.mail.Folder; +import jakarta.mail.FolderClosedException; +import jakarta.mail.MessagingException; +import java.io.IOException; +import java.io.InputStream; +import org.xbib.net.mail.iap.ByteArray; +import org.xbib.net.mail.iap.ConnectionException; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.imap.protocol.BODY; +import org.xbib.net.mail.imap.protocol.IMAPProtocol; + +/** + * 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 IOException { + synchronized (msg.getMessageCacheLock()) { + try { + msg.getProtocol().noop(); + } catch (ConnectionException cex) { + throw new IOException(new FolderClosedException(msg.getFolder(), cex.getMessage())); + } catch (FolderClosedException fex) { + throw new IOException(new FolderClosedException(fex.getFolder(), fex.getMessage())); + } catch (ProtocolException pex) { + // ignore it + } + } + if (msg.isExpunged()) + throw new IOException(new MessagingException()); + } + + /** + * 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 IOException(new MessagingException("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 IOException(new FolderClosedException(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 = Math.min(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/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPMessage.java b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPMessage.java new file mode 100644 index 0000000..cab1e27 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPMessage.java @@ -0,0 +1,1737 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.activation.DataHandler; +import jakarta.mail.Address; +import jakarta.mail.FetchProfile; +import jakarta.mail.Flags; +import jakarta.mail.FolderClosedException; +import jakarta.mail.Header; +import jakarta.mail.IllegalWriteException; +import jakarta.mail.Message; +import jakarta.mail.MessageRemovedException; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.UIDFolder; +import jakarta.mail.internet.ContentType; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.InternetHeaders; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeUtility; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.Date; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Hashtable; +import java.util.Iterator; +import java.util.Locale; +import java.util.Map; +import java.util.Set; +import org.xbib.net.mail.iap.ConnectionException; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.iap.Response; +import org.xbib.net.mail.imap.protocol.BODY; +import org.xbib.net.mail.imap.protocol.BODYSTRUCTURE; +import org.xbib.net.mail.imap.protocol.ENVELOPE; +import org.xbib.net.mail.imap.protocol.FetchItem; +import org.xbib.net.mail.imap.protocol.FetchResponse; +import org.xbib.net.mail.imap.protocol.IMAPProtocol; +import org.xbib.net.mail.imap.protocol.INTERNALDATE; +import org.xbib.net.mail.imap.protocol.Item; +import org.xbib.net.mail.imap.protocol.MODSEQ; +import org.xbib.net.mail.imap.protocol.RFC822DATA; +import org.xbib.net.mail.imap.protocol.RFC822SIZE; +import org.xbib.net.mail.imap.protocol.UID; +import org.xbib.net.mail.util.ReadableMime; + +/** + * 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. + * + * @throws ProtocolException for protocol errors + * @return the IMAPProtocol object for the containing folder + * @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 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 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/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPMultipartDataSource.java b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPMultipartDataSource.java new file mode 100644 index 0000000..61ef8c4 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPMultipartDataSource.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + + +import jakarta.mail.BodyPart; +import jakarta.mail.MessagingException; +import jakarta.mail.MultipartDataSource; +import jakarta.mail.internet.MimePart; +import jakarta.mail.internet.MimePartDataSource; +import java.util.ArrayList; +import java.util.List; +import org.xbib.net.mail.imap.protocol.BODYSTRUCTURE; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPNestedMessage.java b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPNestedMessage.java new file mode 100644 index 0000000..7ea5f13 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPNestedMessage.java @@ -0,0 +1,142 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Flags; +import jakarta.mail.FolderClosedException; +import jakarta.mail.MessageRemovedException; +import jakarta.mail.MessagingException; +import jakarta.mail.MethodNotSupportedException; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.imap.protocol.BODYSTRUCTURE; +import org.xbib.net.mail.imap.protocol.ENVELOPE; +import org.xbib.net.mail.imap.protocol.IMAPProtocol; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPProvider.java b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPProvider.java new file mode 100644 index 0000000..2de01c6 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Provider; +import org.xbib.net.mail.util.DefaultProvider; + +/** + * The IMAP protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class IMAPProvider extends Provider { + public IMAPProvider() { + super(Type.STORE, "imap", IMAPStore.class.getName(), + "Oracle", null); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPSSLProvider.java b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPSSLProvider.java new file mode 100644 index 0000000..8d12a07 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPSSLProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Provider; +import org.xbib.net.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(Type.STORE, "imaps", IMAPSSLStore.class.getName(), + "Oracle", null); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPSSLStore.java b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPSSLStore.java new file mode 100644 index 0000000..94dc0a0 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPSSLStore.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Session; +import jakarta.mail.URLName; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPStore.java b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPStore.java new file mode 100644 index 0000000..2de2b58 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/IMAPStore.java @@ -0,0 +1,2196 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.AuthenticationFailedException; +import jakarta.mail.Folder; +import jakarta.mail.MessagingException; +import jakarta.mail.PasswordAuthentication; +import jakarta.mail.Quota; +import jakarta.mail.QuotaAwareStore; +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.StoreClosedException; +import jakarta.mail.URLName; +import jakarta.mail.event.StoreEvent; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.Vector; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.mail.iap.BadCommandException; +import org.xbib.net.mail.iap.CommandFailedException; +import org.xbib.net.mail.iap.ConnectionException; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.iap.Response; +import org.xbib.net.mail.iap.ResponseHandler; +import org.xbib.net.mail.imap.protocol.IMAPProtocol; +import org.xbib.net.mail.imap.protocol.IMAPReferralException; +import org.xbib.net.mail.imap.protocol.ListInfo; +import org.xbib.net.mail.imap.protocol.Namespaces; +import org.xbib.net.mail.util.MailConnectException; +import org.xbib.net.mail.util.PropUtil; +import org.xbib.net.mail.util.SocketConnectException; + +/** + * 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 org.xbib.net.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 { + + private static final Logger logger = Logger.getLogger(IMAPStore.class.getName()); + + /** + * 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? + + 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; + + /* + * 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, Session session) { + lastTimePruned = System.currentTimeMillis(); + Properties props = session.getProperties(); + + boolean debug = PropUtil.getBooleanProperty(props, + "mail." + name + ".connectionpool.debug", false); + + // 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); + + 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, 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); + } + + 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()) { + 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 (logger.isLoggable(Level.FINE)) + 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; + 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; + } + + /** + * Report whether message cache debugging is enabled. + */ + boolean getMessageCacheDebug() { + return messageCacheDebug; + } + + /** + * Report whether the connection pool is full. + */ + boolean isConnectionPoolFull() { + + synchronized (pool) { + if (logger.isLoggable(Level.FINE)) + 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 + + 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 + + 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(); + } + + 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 (logger.isLoggable(Level.FINE)) { + logger.fine("checking for connections to prune: " + + (System.currentTimeMillis() - pool.lastTimePruned)); + 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 (logger.isLoggable(Level.FINE)) + logger.fine("protocol last used: " + + (System.currentTimeMillis() - p.getTimestamp())); + if (System.currentTimeMillis() - p.getTimestamp() > + pool.clientTimeoutInterval) { + + 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); + } + + /** + * 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. + * + * @throws MessagingException if the server doesn't support the + * QUOTA extension + * @param root the name of the quota root + * @return array of Quota objects + */ + @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. + * + * @throws MessagingException if the server doesn't support the + * QUOTA extension + * @param quota the quota to set + */ + @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. + * + * @throws MessagingException if the server doesn't support the + * IDLE extension + * @throws 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. + * + * @throws MessagingException if the server doesn't support the + * ID extension + * @param clientParams a Map of keys and values identifying the client + * @return a Map of keys and values identifying the server + * @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/net-mail/src/main/java/org/xbib/net/mail/imap/IdleManager.java b/net-mail/src/main/java/org/xbib/net/mail/imap/IdleManager.java new file mode 100644 index 0000000..80cee71 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/IdleManager.java @@ -0,0 +1,492 @@ +/* + * Copyright (c) 2014, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Folder; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import java.io.IOException; +import java.io.InterruptedIOException; +import java.net.Socket; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.SocketChannel; +import java.util.Iterator; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 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();
+ * 		logger.log(Level.INFO, "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 SocketChannel SocketChannel} must be used instead + * of a {@link 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 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 static final Logger logger = Logger.getLogger(IdleManager.class.getName()); + + private Executor es; + private Selector selector; + 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; + selector = Selector.open(); + es.execute(() -> { + 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 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/net-mail/src/main/java/org/xbib/net/mail/imap/MessageCache.java b/net-mail/src/main/java/org/xbib/net/mail/imap/MessageCache.java new file mode 100644 index 0000000..d02118c --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/MessageCache.java @@ -0,0 +1,437 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Message; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 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 { + + private static final Logger logger = Logger.getLogger(MessageCache.class.getName()); + /* + * 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; + + /** + * 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. + */ + public MessageCache(IMAPFolder folder, IMAPStore store, int size) { + this.folder = folder; + if (logger.isLoggable(Level.CONFIG)) { + logger.config("create cache of size " + size); + } + ensureCapacity(size, 1); + } + + /** + * Constructor for debugging and testing. + */ + public MessageCache(int size, boolean debug) { + this.folder = null; + 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/net-mail/src/main/java/org/xbib/net/mail/imap/MessageVanishedEvent.java b/net-mail/src/main/java/org/xbib/net/mail/imap/MessageVanishedEvent.java new file mode 100644 index 0000000..cc15d3e --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/MessageVanishedEvent.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.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 + */ +@SuppressWarnings("serial") +public class MessageVanishedEvent extends MessageCountEvent { + + /** + * The message UIDs. + */ + private long[] uids; + + // a reusable empty array + private static final Message[] noMessages = {}; + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/ModifiedSinceTerm.java b/net-mail/src/main/java/org/xbib/net/mail/imap/ModifiedSinceTerm.java new file mode 100644 index 0000000..130d1ad --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/ModifiedSinceTerm.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Message; +import jakarta.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; + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/OlderTerm.java b/net-mail/src/main/java/org/xbib/net/mail/imap/OlderTerm.java new file mode 100644 index 0000000..fc97add --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/OlderTerm.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Message; +import jakarta.mail.search.SearchTerm; +import java.util.Date; + +/** + * 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; + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/ReferralException.java b/net-mail/src/main/java/org/xbib/net/mail/imap/ReferralException.java new file mode 100644 index 0000000..6d954ec --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/ReferralException.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.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 + */ +@SuppressWarnings("serial") +public class ReferralException extends AuthenticationFailedException { + + private String url; + private String text; + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/ResyncData.java b/net-mail/src/main/java/org/xbib/net/mail/imap/ResyncData.java new file mode 100644 index 0000000..186d285 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/ResyncData.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import org.xbib.net.mail.imap.protocol.UIDSet; + +/** + * Resynchronization data as defined by the QRESYNC extension + * (RFC 5162). + * An instance of ResyncData is supplied to the + * {@link IMAPFolder#open(int, 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/net-mail/src/main/java/org/xbib/net/mail/imap/Rights.java b/net-mail/src/main/java/org/xbib/net/mail/imap/Rights.java new file mode 100644 index 0000000..7f28c19 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/Rights.java @@ -0,0 +1,305 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.imap; + +import java.util.ArrayList; +import java.util.List; + +/** + * 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))
+ * 	logger.log(Level.FINEST, "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 { + + 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 final 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 + */ + @SuppressWarnings("this-escape") + 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. + * + * @return true of the given right is present, otherwise false. + * @param right the Right to check + */ + 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[0]); + } + + @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(); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/SortTerm.java b/net-mail/src/main/java/org/xbib/net/mail/imap/SortTerm.java new file mode 100644 index 0000000..00b7ff2 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/SortTerm.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.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/net-mail/src/main/java/org/xbib/net/mail/imap/Utility.java b/net-mail/src/main/java/org/xbib/net/mail/imap/Utility.java new file mode 100644 index 0000000..572fb87 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/Utility.java @@ -0,0 +1,214 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Message; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import org.xbib.net.mail.imap.protocol.MessageSet; +import org.xbib.net.mail.imap.protocol.UIDSet; + +/** + * 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[0]); + } + } + + /** + * 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[0]); + } + } + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/YoungerTerm.java b/net-mail/src/main/java/org/xbib/net/mail/imap/YoungerTerm.java new file mode 100644 index 0000000..dc303c4 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/YoungerTerm.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap; + +import jakarta.mail.Message; +import jakarta.mail.search.SearchTerm; +import java.util.Date; + +/** + * 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; + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/package-info.java b/net-mail/src/main/java/org/xbib/net/mail/imap/package-info.java new file mode 100644 index 0000000..1b04d3c --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/package-info.java @@ -0,0 +1,1047 @@ +/* + * Copyright (c) 2021, 2024 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 + */ + +/** + * 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 org.xbib.net.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 org.xbib.net.mail.imap.IMAPFolder#addMessages addMessages}, + * {@link org.xbib.net.mail.imap.IMAPFolder#appendUIDMessages appendUIDMessages}, and + * {@link org.xbib.net.mail.imap.IMAPFolder#copyUIDMessages copyUIDMessages}. + *

+ * MOVE Support + *

+ * The IMAP MOVE extension + * (RFC 6851) + * is supported via the IMAPFolder methods + * {@link org.xbib.net.mail.imap.IMAPFolder#moveMessages moveMessages} and + * {@link org.xbib.net.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 jakarta.mail.QuotaAwareStore QuotaAwareStore} interface implemented by + * {@link org.xbib.net.mail.imap.IMAPStore IMAPStore}, and the + * {@link org.xbib.net.mail.imap.IMAPFolder#getQuota IMAPFolder getQuota} and + * {@link org.xbib.net.mail.imap.IMAPFolder#setQuota IMAPFolder setQuota} methods. + * ACL Support + *

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

+ * SORT Support + *

+ * The IMAP SORT extension + * (RFC 5256) + * is supported via the + * {@link org.xbib.net.mail.imap.SortTerm SortTerm} class and the IMAPFolder + * {@link org.xbib.net.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 org.xbib.net.mail.imap.IMAPFolder#open(int, ResyncData) + * IMAPFolder open} method and supplying an appropriate + * {@link org.xbib.net.mail.imap.ResyncData ResyncData} instance. + * Using + * {@link org.xbib.net.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 org.xbib.net.mail.imap.IMAPMessage#getModSeq IMAPMessage getModSeq} + * method and the + * {@link org.xbib.net.mail.imap.IMAPFolder#getHighestModSeq + * IMAPFolder getHighestModSeq} method. + * Using a + * {@link org.xbib.net.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 jakarta.mail.event.MailEvent MailEvent} instances. + * The special + * {@link org.xbib.net.mail.imap.MessageVanishedEvent MessageVanishedEvent} reports on + * UIDs of messages that have been removed since the last resynchronization. + * A + * {@link jakarta.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 org.xbib.net.mail.imap.YoungerTerm YoungerTerm} and + * {@link org.xbib.net.mail.imap.OlderTerm OlderTerm} + * {@link jakarta.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 org.xbib.net.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 org.xbib.net.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.executor.writetimeoutjava.util.concurrent.ScheduledExecutorService Provides specific ScheduledExecutorService for mail.imap.writetimeout option. + * The value of mail.imap.writetimeout shouldn't be a null. + * For provided executor pool it is highly recommended to have set up in true + * {@link java.util.concurrent.ScheduledThreadPoolExecutor#setRemoveOnCancelPolicy(boolean)}. + * Without it, write methods will create garbage that would only be reclaimed after the timeout. + * Be careful with calling {@link java.util.concurrent.ScheduledThreadPoolExecutor#shutdownNow()} in your executor, + * it can kill the running tasks. It would be ok to use shutdownNow only when JavaMail sockets are closed. + * This would be all service subclasses ({@link jakarta.mail.Store}/{@link jakarta.mail.Transport}) + * Invoking run {@link java.lang.Runnable#run()} on the returned {@link java.util.concurrent.Future} objects + * would force close the open connections. + * Instead of shutdownNow you can use {@link java.util.concurrent.ScheduledThreadPoolExecutor#shutdown()} ()} + * and + * {@link java.util.concurrent.ScheduledThreadPoolExecutor#awaitTermination(long, java.util.concurrent.TimeUnit)} ()}. + *
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 false, it does not check the server identity as specified by + * RFC 2595, + * RFC 2830, and + * RFC 6125. + * These additional checks based on the content of the server's certificate + * are intended to prevent man-in-the-middle attacks. + * Defaults to true. + *
mail.imap.ssl.hostnameverifierjavax.net.ssl.HostnameVerifier + * If set to an object that implements the + * javax.net.ssl.HostnameVerifier interface then, this object + * will be used to verify the hostname against the certificate. Note that this + * is an instance of a class, not a name, and must be set using the + * put method, not the setProperty method. The given + * object will provide additional checks based on the content of the server's + * certificate are intended to prevent man-in-the-middle attacks. Defaults to + * null. + *
mail.imap.ssl.hostnameverifier.classString + * If set, specifies the name of a class that implements the + * javax.net.ssl.HostnameVerifier interface or an alias name + * assigned to a built in hostname verifier. A class name will be instantiated + * using the default constructor and that instance will be used to verify the + * hostname against the certificate. The alias name "legacy" will + * enable the "sun.security.util.HostnameChecker" with fail over to + * the "MailHostnameVerifier". The alias name + * "sun.security.util.HostnameChecker" or + * "JdkHostnameChecker" will attempt to access the + * sun.security.util.HostnameChecker via reflection. The alias name + * "MailHostnameVerifier" will check server identity as specified + * by RFC 2595. + * The instantiated object will provide additional checks based on the content + * of the server's certificate are intended to prevent man-in-the-middle + * attacks. Defaults to null. + *
mail.imap.ssl.trustString + * If set, and a socket factory hasn't been specified, enables use of a + * {@link org.xbib.net.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 jakarta.mail.search.SearchTerm SearchTerm} + * passed to the + * {@link jakarta.mail.Folder#search Folder.search} + * method is too complex for the IMAP protocol, throw a + * {@link jakarta.mail.search.SearchException SearchException}. + * For example, the IMAP protocol only supports less-than and greater-than + * comparisons for a {@link jakarta.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 org.xbib.net.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 org.xbib.net.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 + * jakarta.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 jakarta.mail.Session Session} configuration, + * the org.xbib.net.mail.imap provider logs the same information using + * {@link java.util.logging.Logger} as described in the following table: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
IMAP Loggers
Logger NameLogging LevelPurpose
org.xbib.net.mail.imapCONFIGConfiguration of the IMAPStore
org.xbib.net.mail.imapFINEGeneral debugging output
org.xbib.net.mail.imap.connectionpoolCONFIGConfiguration of the IMAP connection pool
org.xbib.net.mail.imap.connectionpoolFINEDebugging output related to the IMAP connection pool
org.xbib.net.mail.imap.messagecacheCONFIGConfiguration of the IMAP message cache
org.xbib.net.mail.imap.messagecacheFINEDebugging output related to the IMAP message cache
org.xbib.net.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. + */ +package org.xbib.net.mail.imap; diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BASE64MailboxDecoder.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BASE64MailboxDecoder.java new file mode 100644 index 0000000..81221e4 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BASE64MailboxDecoder.java @@ -0,0 +1,177 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; + +/** + * 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 { + + /** + * Creates a default {@code BASE64MailboxDecoder}. + * This constructor should never be invoked. + */ + private BASE64MailboxDecoder() { + } + + public static String decode(String original) { + if (original == null || original.isEmpty()) + 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BASE64MailboxEncoder.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BASE64MailboxEncoder.java new file mode 100644 index 0000000..b920b9f --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BASE64MailboxEncoder.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import java.io.CharArrayWriter; +import java.io.IOException; +import java.io.Writer; + + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BODY.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BODY.java new file mode 100644 index 0000000..a42796d --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BODY.java @@ -0,0 +1,95 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import java.io.ByteArrayInputStream; +import org.xbib.net.mail.iap.ByteArray; +import org.xbib.net.mail.iap.ParsingException; + +/** + * 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 + * + * @throws ParsingException for parsing failures + * @param r the FetchResponse + */ + 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; + } + + /** + * @return origin + * @since Jakarta Mail 1.6.4 + */ + public int getOrigin() { + return origin; + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BODYSTRUCTURE.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BODYSTRUCTURE.java new file mode 100644 index 0000000..cfcf46f --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/BODYSTRUCTURE.java @@ -0,0 +1,454 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import jakarta.mail.internet.ParameterList; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.mail.iap.ParsingException; +import org.xbib.net.mail.iap.Response; +import org.xbib.net.mail.util.PropUtil; + +/** + * A BODYSTRUCTURE response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class BODYSTRUCTURE implements Item { + + private static final Logger logger = Logger.getLogger(BODYSTRUCTURE.class.getName()); + + 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) + logger.log(Level.FINEST, "DEBUG IMAP: parsing BODYSTRUCTURE"); + msgno = r.getNumber(); + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: msgno " + msgno); + + r.skipSpaces(); + + if (r.readByte() != '(') + throw new ParsingException( + "BODYSTRUCTURE parse error: missing ``('' at start"); + + if (r.peekByte() == '(') { // multipart + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: parsing multipart"); + type = "multipart"; + processedType = MULTI; + List v = new ArrayList<>(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[0]); + + subtype = r.readString(); // subtype + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: subtype " + subtype); + + if (r.isNextNonSpace(')')) { // done + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: parse DONE"); + return; + } + + // Else, we have extension data + + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: parsing extension data"); + // Body parameters + cParams = parseParameters(r); + if (r.isNextNonSpace(')')) { // done + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: body parameters DONE"); + return; + } + + // Disposition + byte b = r.peekByte(); + if (b == '(') { + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: parse disposition"); + r.readByte(); + disposition = r.readString(); + if (parseDebug) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "DEBUG IMAP: disposition DONE"); + } else if (b == 'N' || b == 'n') { + if (parseDebug) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "DEBUG IMAP: bad multipart disposition" + + ", applying Exchange bug workaround"); + description = r.readString(); + if (parseDebug) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "DEBUG IMAP: no body-fld-lang"); + return; // done + } + + // Language + if (r.peekByte() == '(') { // a list follows + language = r.readStringList(); + if (parseDebug) + logger.log(Level.FINEST, + "DEBUG IMAP: language len " + language.length); + } else { + String l = r.readString(); + if (l != null) { + String[] la = {l}; + language = la; + if (parseDebug) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "DEBUG IMAP: single part"); + type = r.readString(); + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: type " + type); + processedType = SINGLE; + subtype = r.readString(); + if (parseDebug) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "DEBUG IMAP: cParams " + cParams); + id = r.readString(); + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: id " + id); + description = r.readString(); + if (parseDebug) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "DEBUG IMAP: encoding " + encoding); + size = r.readNumber(); + if (parseDebug) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, + "DEBUG IMAP: got envelope of nested message"); + BODYSTRUCTURE[] bs = {new BODYSTRUCTURE(r)}; + bodies = bs; + lines = r.readNumber(); + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: lines " + lines); + if (lines < 0) + throw new ParsingException( + "BODYSTRUCTURE parse error: bad ``lines'' element"); + } else { + if (parseDebug) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "DEBUG IMAP: parse DONE"); + return; // done + } + + // Optional extension data + + // MD5 + md5 = r.readString(); + if (r.isNextNonSpace(')')) { + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: no MD5 DONE"); + return; // done + } + + // Disposition + byte b = r.readByte(); + if (b == '(') { + disposition = r.readString(); + if (parseDebug) + logger.log(Level.FINEST, "DEBUG IMAP: disposition " + + disposition); + dParams = parseParameters(r); + if (parseDebug) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "DEBUG IMAP: disposition DONE"); + return; // done + } + + // Language + if (r.peekByte() == '(') { // a list follows + language = r.readStringList(); + if (parseDebug) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "DEBUG IMAP: parameter value " + value); + if (value == null) { // work around buggy servers + if (parseDebug) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, "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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ENVELOPE.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ENVELOPE.java new file mode 100644 index 0000000..0063e5b --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ENVELOPE.java @@ -0,0 +1,225 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MailDateFormat; +import java.text.ParseException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.mail.iap.ParsingException; +import org.xbib.net.mail.iap.Response; +import org.xbib.net.mail.util.PropUtil; + +/** + * The ENEVELOPE item of an IMAP FETCH response. + * + * @author John Mani + * @author Bill Shannon + */ + +public class ENVELOPE implements Item { + + private static final Logger logger = Logger.getLogger(ENVELOPE.class.getName()); + + // 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) + logger.log(Level.FINEST, "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) + logger.log(Level.FINEST, " Date: " + date); + + subject = r.readString(); + if (parseDebug) + logger.log(Level.FINEST, " Subject: " + subject); + if (parseDebug) + logger.log(Level.FINEST, " From addresses:"); + from = parseAddressList(r); + if (parseDebug) + logger.log(Level.FINEST, " Sender addresses:"); + sender = parseAddressList(r); + if (parseDebug) + logger.log(Level.FINEST, " Reply-To addresses:"); + replyTo = parseAddressList(r); + if (parseDebug) + logger.log(Level.FINEST, " To addresses:"); + to = parseAddressList(r); + if (parseDebug) + logger.log(Level.FINEST, " Cc addresses:"); + cc = parseAddressList(r); + if (parseDebug) + logger.log(Level.FINEST, " Bcc addresses:"); + bcc = parseAddressList(r); + inReplyTo = r.readString(); + if (parseDebug) + logger.log(Level.FINEST, " In-Reply-To: " + inReplyTo); + messageId = r.readString(); + if (parseDebug) + logger.log(Level.FINEST, " 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) + logger.log(Level.FINEST, " 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[0]); + } 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; + + 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[0]); + } 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FLAGS.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FLAGS.java new file mode 100644 index 0000000..3f4fe42 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FLAGS.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.imap.protocol; + +import jakarta.mail.Flags; +import org.xbib.net.mail.iap.ParsingException; + +/** + * 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; + + /** + * Constructor. + * + * @throws ParsingException for parsing failures + * @param r the IMAPResponse + */ + @SuppressWarnings("this-escape") + 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(Flag.SEEN); + break; + case 'R': // \Recent + add(Flag.RECENT); + break; + case 'D': + if (s.length() >= 3) { + char c = s.charAt(2); + if (c == 'e' || c == 'E') // \Deleted + add(Flag.DELETED); + else if (c == 'r' || c == 'R') // \Draft + add(Flag.DRAFT); + } else + add(s); // unknown, treat it as a user flag + break; + case 'A': // \Answered + add(Flag.ANSWERED); + break; + case 'F': // \Flagged + add(Flag.FLAGGED); + break; + case '*': // \* + add(Flag.USER); + break; + default: + add(s); // unknown, treat it as a user flag + break; + } + } else + add(s); + } + } + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FetchItem.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FetchItem.java new file mode 100644 index 0000000..4ef583a --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FetchItem.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import jakarta.mail.FetchProfile; +import org.xbib.net.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. + * + * @throws ParsingException for parsing failures + * @param r the response + * @return the fetch item + */ + public abstract Object parseItem(FetchResponse r) throws ParsingException; +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FetchResponse.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FetchResponse.java new file mode 100644 index 0000000..42955fb --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/FetchResponse.java @@ -0,0 +1,334 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.imap.protocol; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.xbib.net.mail.iap.ParsingException; +import org.xbib.net.mail.iap.Protocol; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.iap.Response; +import org.xbib.net.mail.util.ASCIIUtility; + +/** + * 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; + + @SuppressWarnings("this-escape") + 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 + */ + @SuppressWarnings("this-escape") + 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<>(); + skipSpaces(); + do { + + if (index >= size) + throw new ParsingException( + "error in FETCH parsing, ran off end of buffer, size " + size); + + Item 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[0]); + } + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ID.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ID.java new file mode 100644 index 0000000..87b7548 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ID.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.xbib.net.mail.iap.Argument; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.iap.Response; + +/** + * This class represents the response to the ID command.

+ * + * See RFC 2971. + * + * @author Bill Shannon + * @since JavaMail 1.5.1 + */ + +public class ID { + + private Map serverParams = null; + + /** + * Parse the server parameter list out of the response. + * + * @throws ProtocolException for protocol failures + * @param r the response + */ + 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPProtocol.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPProtocol.java new file mode 100644 index 0000000..7a9453f --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPProtocol.java @@ -0,0 +1,3117 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.imap.protocol; + +import jakarta.mail.Flags; +import jakarta.mail.Folder; +import jakarta.mail.Quota; +import jakarta.mail.UIDFolder; +import jakarta.mail.internet.MimeUtility; +import jakarta.mail.search.SearchException; +import jakarta.mail.search.SearchTerm; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.lang.reflect.Constructor; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.mail.iap.Argument; +import org.xbib.net.mail.iap.BadCommandException; +import org.xbib.net.mail.iap.ByteArray; +import org.xbib.net.mail.iap.CommandFailedException; +import org.xbib.net.mail.iap.ConnectionException; +import org.xbib.net.mail.iap.Literal; +import org.xbib.net.mail.iap.LiteralException; +import org.xbib.net.mail.iap.ParsingException; +import org.xbib.net.mail.iap.Protocol; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.iap.Response; +import org.xbib.net.mail.imap.ACL; +import org.xbib.net.mail.imap.AppendUID; +import org.xbib.net.mail.imap.CopyUID; +import org.xbib.net.mail.imap.ResyncData; +import org.xbib.net.mail.imap.Rights; +import org.xbib.net.mail.imap.SortTerm; +import org.xbib.net.mail.imap.Utility; +import org.xbib.net.mail.util.ASCIIUtility; +import org.xbib.net.mail.util.BASE64EncoderStream; +import org.xbib.net.mail.util.PropUtil; + +/** + * 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 static final Logger logger = Logger.getLogger(IMAPProtocol.class.getName()); + + 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. + * + * @throws ProtocolException for protocol failures + * @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 + * @exception IOException for I/O errors + */ + @SuppressWarnings("this-escape") + public IMAPProtocol(String name, String host, int port, + Properties props, boolean isSSL) + throws IOException, ProtocolException { + super(host, port, props, "mail." + name, isSSL); + 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 = command("LOGIN", args); + // handle an illegal but not uncommon untagged CAPABILTY response + handleCapabilityResponse(r); + // dispatch untagged responses + notifyResponseHandlers(r); + // Handle result of this command + 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 { + 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); + } + + Response[] responses = v.toArray(new Response[0]); + + // 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 + 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 { + 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); + } + + Response[] responses = v.toArray(new Response[0]); + + // 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 + 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 { + Argument args = new Argument(); + args.writeAtom("XOAUTH2"); + if (hasCapability("SASL-IR")) { + String resp = "user=" + u + "\001auth=Bearer " + p + "\001\001"; + byte[] ba = Base64.getEncoder().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 = Base64.getEncoder().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); + } + + Response[] responses = v.toArray(new Response[0]); + + // 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 + 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( + "org.xbib.net.mail.imap.protocol.IMAPSaslAuthenticator"); + Constructor c = sac.getConstructor(IMAPProtocol.class, + String.class, + Properties.class, + String.class); + saslAuthenticator = (SaslAuthenticator) c.newInstance( + new Object[]{ + this, + name, + props, + 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[0]); + + if (saslAuthenticator.authenticate(mechs, realm, authzid, u, p)) { + authenticated = true; + } + } + + // 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 + * @since JavaMail 1.5.1 + * @see "Netscape/iPlanet/SunONE Messaging Server extension" + */ + public void unauthenticate() throws ProtocolException { + if (!hasCapability("X-UNAUTHENTICATE")) + throw new BadCommandException("UNAUTHENTICATE not supported"); + simpleCommand("UNAUTHENTICATE", null); + authenticated = false; + } + + /** + * 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 + * @since JavaMail 1.5.1 + * @see "RFC2060, section 6.3.1" + * @see "RFC5162, section 3.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().contains("READ-ONLY")) + 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 + * @since JavaMail 1.5.1 + * @see "RFC2060, section 6.3.2" + * @see "RFC5162, section 3.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 + * @since JavaMail 1.5.1 + * @see "RFC 5161" + */ + 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 + * @since JavaMail 1.5.1 + * @see "RFC 5161" + */ + 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 + * @since JavaMail 1.4.4 + * @see "RFC 3691" + */ + 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[0]); + } + } + + // 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 + * @since JavaMail 1.5.4 + * @see "RFC 6851" + */ + 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 + * @since JavaMail 1.5.4 + * @see "RFC 6851" + */ + 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 + * @since JavaMail 1.5.4 + * @see "RFC 6851" + * @see "RFC 4315, section 3" + */ + 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 + * @since JavaMail 1.5.4 + * @see "RFC 6851" + * @see "RFC 4315, section 3" + */ + 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. + * + * @since JavaMail 1.5.4 + * @see "RFC 6851" + * @see "RFC 4315, section 3" + */ + 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 | SearchException pex) { + throw pex; + } + } + + // 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[0]); + } + + /** + * 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[0]); + } + + /** + * 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[0]); + 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[0]); + } + + /** + * 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[0]); + } + + /** + * 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 + * @since JavaMail 1.4.1 + * @see "RFC2177" + */ + 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[0]); + 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 | ProtocolException ioex) { + // convert this into a BYE response + r = Response.byeResponse(ioex); + } + 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 + * @since JavaMail 1.5.1 + * @see "RFC 2971" + */ + 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPReferralException.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPReferralException.java new file mode 100644 index 0000000..2d4a1a2 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPReferralException.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import org.xbib.net.mail.iap.ProtocolException; + +/** + * A ProtocolException that includes IMAP login referral information. + * + * @since JavaMail 1.5.5 + */ +@SuppressWarnings("serial") +public class IMAPReferralException extends ProtocolException { + + private String url; + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPResponse.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPResponse.java new file mode 100644 index 0000000..a92abba --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPResponse.java @@ -0,0 +1,146 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.imap.protocol; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import org.xbib.net.mail.iap.Protocol; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.iap.Response; +import org.xbib.net.mail.util.ASCIIUtility; + +/** + * 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; + + @SuppressWarnings("this-escape") + 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 + */ + @SuppressWarnings("this-escape") + 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPSaslAuthenticator.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPSaslAuthenticator.java new file mode 100644 index 0000000..6c241b3 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/IMAPSaslAuthenticator.java @@ -0,0 +1,295 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.sasl.RealmCallback; +import javax.security.sasl.RealmChoiceCallback; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; +import org.xbib.net.security.auth.OAuth2SaslClientFactory; +import org.xbib.net.mail.iap.Argument; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.iap.Response; +import org.xbib.net.mail.util.ASCIIUtility; +import org.xbib.net.mail.util.PropUtil; + +/** + * This class contains a single method that does authentication using + * SASL. This is in a separate class so that it can be compiled with + * J2SE 1.5. Eventually it should be merged into IMAPProtocol.java. + */ + +public class IMAPSaslAuthenticator implements SaslAuthenticator { + + private static final Logger logger = Logger.getLogger(IMAPSaslAuthenticator.class.getName()); + + private IMAPProtocol pr; + private String name; + private Properties props; + private String host; + + /* + * This is a hack to initialize the OAUTH SASL provider just before, + * and only if, we might need it. This avoids the need for the user + * to initialize it explicitly, or manually configure the security + * providers file. + */ + static { + try { + OAuth2SaslClientFactory.init(); + } catch (Throwable t) { + } + } + + public IMAPSaslAuthenticator(IMAPProtocol pr, String name, Properties props, String host) { + this.pr = pr; + this.name = name; + this.props = props; + this.host = host; + } + + @Override + public boolean authenticate(String[] mechs, final String realm, + final String authzid, final String u, + final String p) throws ProtocolException { + + synchronized (pr) { // authenticate method should be synchronized + List v = new ArrayList<>(); + String tag = null; + Response r = null; + boolean done = false; + if (logger.isLoggable(Level.FINE)) { + logger.fine("SASL Mechanisms:"); + for (int i = 0; i < mechs.length; i++) + logger.fine(" " + mechs[i]); + logger.fine(""); + } + + SaslClient sc; + CallbackHandler cbh = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL callback length: " + callbacks.length); + for (int i = 0; i < callbacks.length; i++) { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL callback " + i + ": " + callbacks[i]); + if (callbacks[i] instanceof NameCallback) { + NameCallback ncb = (NameCallback) callbacks[i]; + ncb.setName(u); + } else if (callbacks[i] instanceof PasswordCallback) { + PasswordCallback pcb = (PasswordCallback) callbacks[i]; + pcb.setPassword(p.toCharArray()); + } else if (callbacks[i] instanceof RealmCallback) { + RealmCallback rcb = (RealmCallback) callbacks[i]; + rcb.setText(realm != null ? + realm : rcb.getDefaultText()); + } else if (callbacks[i] instanceof RealmChoiceCallback) { + RealmChoiceCallback rcb = + (RealmChoiceCallback) callbacks[i]; + if (realm == null) + rcb.setSelectedIndex(rcb.getDefaultChoice()); + else { + // need to find specified realm in list + String[] choices = rcb.getChoices(); + for (int k = 0; k < choices.length; k++) { + if (choices[k].equals(realm)) { + rcb.setSelectedIndex(k); + break; + } + } + } + } + } + } + }; + + try { + @SuppressWarnings("unchecked") + Map propsMap = (Map) props; + sc = Sasl.createSaslClient(mechs, authzid, name, host, + propsMap, cbh); + } catch (SaslException sex) { + logger.log(Level.FINE, "Failed to create SASL client", sex); + throw new UnsupportedOperationException(sex.getMessage(), sex); + } + if (sc == null) { + logger.fine("No SASL support"); + throw new UnsupportedOperationException("No SASL support"); + } + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL client " + sc.getMechanismName()); + + try { + Argument args = new Argument(); + args.writeAtom(sc.getMechanismName()); + if (pr.hasCapability("SASL-IR") && sc.hasInitialResponse()) { + String irs; + byte[] ba = sc.evaluateChallenge(new byte[0]); + if (ba.length > 0) { + ba = Base64.getEncoder().encode(ba); + irs = ASCIIUtility.toString(ba, 0, ba.length); + } else + irs = "="; + args.writeAtom(irs); + } + tag = pr.writeCommand("AUTHENTICATE", args); + } catch (Exception ex) { + logger.log(Level.FINE, "SASL AUTHENTICATE Exception", ex); + return false; + } + + OutputStream os = pr.getIMAPOutputStream(); // 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 ! + */ + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + byte[] CRLF = {(byte) '\r', (byte) '\n'}; + + // Hack for Novell GroupWise XGWTRUSTEDAPP authentication mechanism + // http://www.novell.com/developer/documentation/gwimap/? + // page=/developer/documentation/gwimap/gwimpenu/data/al7te9j.html + boolean isXGWTRUSTEDAPP = + sc.getMechanismName().equals("XGWTRUSTEDAPP") && + PropUtil.getBooleanProperty(props, + "mail." + name + ".sasl.xgwtrustedapphack.enable", true); + while (!done) { // loop till we are done + try { + r = pr.readResponse(); + if (r.isContinuation()) { + byte[] ba = null; + if (!sc.isComplete()) { + ba = r.readByteArray().getNewBytes(); + if (ba.length > 0) + ba = Base64.getDecoder().decode(ba); + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL challenge: " + + ASCIIUtility.toString(ba, 0, ba.length) + " :"); + ba = sc.evaluateChallenge(ba); + } + if (ba == null) { + logger.fine("SASL no response"); + os.write(CRLF); // write out empty line + os.flush(); // flush the stream + bos.reset(); // reset buffer + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL response: " + + ASCIIUtility.toString(ba, 0, ba.length) + " :"); + ba = Base64.getEncoder().encode(ba); + if (isXGWTRUSTEDAPP) + bos.write(ASCIIUtility.getBytes("XGWTRUSTEDAPP ")); + bos.write(ba); + + 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; + else // hmm .. unsolicited response here ?! + v.add(r); + } catch (Exception ioex) { + logger.log(Level.FINE, "SASL Exception", ioex); + // convert this into a BYE response + r = Response.byeResponse(ioex); + done = true; + // XXX - ultimately return true??? + } + } + + if (sc.isComplete() /*&& res.status == SUCCESS*/) { + String qop = (String) sc.getNegotiatedProperty(Sasl.QOP); + if (qop != null && (qop.equalsIgnoreCase("auth-int") || + qop.equalsIgnoreCase("auth-conf"))) { + // XXX - NOT SUPPORTED!!! + logger.fine( + "SASL Mechanism requires integrity or confidentiality"); + return false; + } + } + + Response[] responses = v.toArray(new Response[0]); + + // handle an illegal but not uncommon untagged CAPABILTY response + pr.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. + */ + pr.notifyResponseHandlers(responses); + + // Handle the final OK, NO, BAD or BYE response + pr.handleLoginResult(r); + pr.setCapabilities(r); + + /* + * If we're using the Novell Groupwise XGWTRUSTEDAPP mechanism + * to run as a specified authorization ID, we have to issue a + * LOGIN command to select the user we want to operate as. + */ + if (isXGWTRUSTEDAPP && authzid != null) { + Argument args = new Argument(); + args.writeString(authzid); + + responses = pr.command("LOGIN", args); + + // dispatch untagged responses + pr.notifyResponseHandlers(responses); + + // Handle result of this command + pr.handleResult(responses[responses.length - 1]); + // If the response includes a CAPABILITY response code, process it + pr.setCapabilities(responses[responses.length - 1]); + } + return true; + } + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/INTERNALDATE.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/INTERNALDATE.java new file mode 100644 index 0000000..b0a4cdd --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/INTERNALDATE.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import jakarta.mail.internet.MailDateFormat; +import java.text.FieldPosition; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import org.xbib.net.mail.iap.ParsingException; + + +/** + * 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. + * + * @throws ParsingException for parsing failures + * @param r the FetchResponse + */ + 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Item.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Item.java new file mode 100644 index 0000000..bb502c4 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Item.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ListInfo.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ListInfo.java new file mode 100644 index 0000000..80e7273 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/ListInfo.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import java.util.ArrayList; +import java.util.List; +import org.xbib.net.mail.iap.ParsingException; + +/** + * 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[0]); + + 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MODSEQ.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MODSEQ.java new file mode 100644 index 0000000..13774c8 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MODSEQ.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import org.xbib.net.mail.iap.ParsingException; + +/** + * 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. + * + * @throws ParsingException for parsing failures + * @param r the FetchResponse + */ + 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MailboxInfo.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MailboxInfo.java new file mode 100644 index 0000000..7b352eb --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MailboxInfo.java @@ -0,0 +1,184 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import jakarta.mail.Flags; +import java.util.ArrayList; +import java.util.List; +import org.xbib.net.mail.iap.ParsingException; +import org.xbib.net.mail.iap.Response; + +/** + * 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. + * + * @throws ParsingException for errors parsing the responses + * @param r 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MessageSet.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MessageSet.java new file mode 100644 index 0000000..4a84c1a --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/MessageSet.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import java.util.ArrayList; +import java.util.List; + +/** + * 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[0]); + } + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Namespaces.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Namespaces.java new file mode 100644 index 0000000..47c9c59 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Namespaces.java @@ -0,0 +1,151 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import java.util.ArrayList; +import java.util.List; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.iap.Response; + +/** + * 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. + * + * @throws ProtocolException for any protocol errors + * @param r the Response to parse + */ + 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[0]); + } 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/RFC822DATA.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/RFC822DATA.java new file mode 100644 index 0000000..45d54a6 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/RFC822DATA.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import java.io.ByteArrayInputStream; +import org.xbib.net.mail.iap.ByteArray; +import org.xbib.net.mail.iap.ParsingException; + +/** + * 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. + * + * @throws ParsingException for parsing failures + * @param r the FetchResponse + */ + 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/RFC822SIZE.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/RFC822SIZE.java new file mode 100644 index 0000000..0ae8010 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/RFC822SIZE.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import org.xbib.net.mail.iap.ParsingException; + +/** + * 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. + * + * @throws ParsingException for parsing failures + * @param r the FetchResponse + */ + public RFC822SIZE(FetchResponse r) throws ParsingException { + msgno = r.getNumber(); + r.skipSpaces(); + size = r.readLong(); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/SaslAuthenticator.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/SaslAuthenticator.java new file mode 100644 index 0000000..d9d0007 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/SaslAuthenticator.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import org.xbib.net.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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/SearchSequence.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/SearchSequence.java new file mode 100644 index 0000000..69e0e37 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/SearchSequence.java @@ -0,0 +1,545 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import jakarta.mail.Flags; +import jakarta.mail.Message; +import jakarta.mail.search.AddressTerm; +import jakarta.mail.search.AndTerm; +import jakarta.mail.search.BodyTerm; +import jakarta.mail.search.ComparisonTerm; +import jakarta.mail.search.DateTerm; +import jakarta.mail.search.FlagTerm; +import jakarta.mail.search.FromStringTerm; +import jakarta.mail.search.FromTerm; +import jakarta.mail.search.HeaderTerm; +import jakarta.mail.search.MessageIDTerm; +import jakarta.mail.search.NotTerm; +import jakarta.mail.search.OrTerm; +import jakarta.mail.search.ReceivedDateTerm; +import jakarta.mail.search.RecipientStringTerm; +import jakarta.mail.search.RecipientTerm; +import jakarta.mail.search.SearchException; +import jakarta.mail.search.SearchTerm; +import jakarta.mail.search.SentDateTerm; +import jakarta.mail.search.SizeTerm; +import jakarta.mail.search.StringTerm; +import jakarta.mail.search.SubjectTerm; +import java.io.IOException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import org.xbib.net.mail.iap.Argument; +import org.xbib.net.mail.imap.ModifiedSinceTerm; +import org.xbib.net.mail.imap.OlderTerm; +import org.xbib.net.mail.imap.YoungerTerm; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Status.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Status.java new file mode 100644 index 0000000..dcf1c05 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/Status.java @@ -0,0 +1,144 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import org.xbib.net.mail.iap.ParsingException; +import org.xbib.net.mail.iap.Response; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/UID.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/UID.java new file mode 100644 index 0000000..5d53770 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/UID.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import org.xbib.net.mail.iap.ParsingException; + +/** + * 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. + * + * @throws ParsingException for parsing failures + * @param r the FetchResponse + */ + public UID(FetchResponse r) throws ParsingException { + seqnum = r.getNumber(); + r.skipSpaces(); + uid = r.readLong(); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/UIDSet.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/UIDSet.java new file mode 100644 index 0000000..e12d120 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/UIDSet.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.imap.protocol; + +import java.util.ArrayList; +import java.util.List; +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); + 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/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/package-info.java b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/package-info.java new file mode 100644 index 0000000..bc0b933 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/imap/protocol/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * This package includes internal IMAP support classes and + * SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. + */ +package org.xbib.net.mail.imap.protocol; diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/ContentLengthCounter.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/ContentLengthCounter.java new file mode 100644 index 0000000..665f668 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/ContentLengthCounter.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * Count the number of bytes in the body of the message written to the stream. + */ +class ContentLengthCounter extends OutputStream { + private long size = 0; + private boolean inHeader = true; + private int lastb1 = -1, lastb2 = -1; + + public void write(int b) throws IOException { + if (inHeader) { + // if line terminator is CR + if (b == '\r' && lastb1 == '\r') + inHeader = false; + else if (b == '\n') { + // if line terminator is \n + if (lastb1 == '\n') + inHeader = false; + // if line terminator is CRLF + else if (lastb1 == '\r' && lastb2 == '\n') + inHeader = false; + } + lastb2 = lastb1; + lastb1 = b; + } else + size++; + } + + public void write(byte[] b) throws IOException { + if (inHeader) + super.write(b); + else + size += b.length; + } + + public void write(byte[] b, int off, int len) throws IOException { + if (inHeader) + super.write(b, off, len); + else + size += len; + } + + public long getSize() { + return size; + } + + /* + public static void main(String argv[]) throws Exception { + int b; + ContentLengthCounter os = new ContentLengthCounter(); + while ((b = System.in.read()) >= 0) + os.write(b); + System.out.println("size " + os.getSize()); + } + */ +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/ContentLengthUpdater.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/ContentLengthUpdater.java new file mode 100644 index 0000000..a17e163 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/ContentLengthUpdater.java @@ -0,0 +1,119 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +/** + * Update the Content-Length header in the message written to the stream. + */ +class ContentLengthUpdater extends FilterOutputStream { + private String contentLength; + private boolean inHeader = true; + private boolean sawContentLength = false; + private int lastb1 = -1, lastb2 = -1; + private StringBuilder line = new StringBuilder(); + + public ContentLengthUpdater(OutputStream os, long contentLength) { + super(os); + this.contentLength = "Content-Length: " + contentLength; + } + + public void write(int b) throws IOException { + if (inHeader) { + String eol = "\n"; + // First, determine if we're still in the header. + if (b == '\r') { + // if line terminator is CR + if (lastb1 == '\r') { + inHeader = false; + eol = "\r"; + // else, if line terminator is CRLF + } else if (lastb1 == '\n' && lastb2 == '\r') { + inHeader = false; + eol = "\r\n"; + } + // else, if line terminator is \n + } else if (b == '\n') { + if (lastb1 == '\n') { + inHeader = false; + eol = "\n"; + } + } + + // If we're no longer in the header, and we haven't seen + // a Content-Length header yet, it's time to put one out. + if (!inHeader && !sawContentLength) { + out.write(contentLength.getBytes(StandardCharsets.ISO_8859_1)); + out.write(eol.getBytes(StandardCharsets.ISO_8859_1)); + } + + // If we have a full line, see if it's a Content-Length header. + if (b == '\r' || (b == '\n' && lastb1 != '\r')) { + if (line.toString().regionMatches(true, 0, + "content-length:", 0, 15)) { + // yup, got it + sawContentLength = true; + // put out the new version + out.write(contentLength.getBytes(StandardCharsets.ISO_8859_1)); + } else { + // not a Content-Length header, just write it out + out.write(line.toString().getBytes(StandardCharsets.ISO_8859_1)); + } + line.setLength(0); // clear buffer for next line + } + if (b == '\r' || b == '\n') + out.write(b); // write out line terminator immediately + else + line.append((char) b); // accumulate characters of the line + + // rotate saved characters for next time through loop + lastb2 = lastb1; + lastb1 = b; + } else + out.write(b); // not in the header, just write it out + } + + public void write(byte[] b) throws IOException { + if (inHeader) + write(b, 0, b.length); + else + out.write(b); + } + + public void write(byte[] b, int off, int len) throws IOException { + if (inHeader) { + for (int i = 0; i < len; i++) { + write(b[off + i]); + } + } else + out.write(b, off, len); + } + + // for testing + public static void main(String[] argv) throws Exception { + int b; + ContentLengthUpdater os = + new ContentLengthUpdater(System.out, Long.parseLong(argv[0])); + while ((b = System.in.read()) >= 0) + os.write(b); + os.flush(); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/DefaultMailbox.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/DefaultMailbox.java new file mode 100644 index 0000000..6113af3 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/DefaultMailbox.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.mbox; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +public class DefaultMailbox extends Mailbox { + private final String home; + + private static final boolean homeRelative = + Boolean.getBoolean("mail.mbox.homerelative"); + + public DefaultMailbox() { + home = System.getProperty("user.home"); + } + + @Override + public MailFile getMailFile(String user, String folder) { + return new DefaultMailFile(filename(user, folder)); + } + + @Override + public String filename(String user, String folder) { + try { + char c = folder.charAt(0); + if (c == File.separatorChar) { + return folder; + } else if (c == '~') { + int i = folder.indexOf(File.separatorChar); + String tail = ""; + if (i > 0) { + tail = folder.substring(i); + folder = folder.substring(0, i); + } + return home + tail; + } else { + if (folder.equalsIgnoreCase("INBOX")) + folder = "INBOX"; + if (homeRelative) + return home + File.separator + folder; + else + return folder; + } + } catch (StringIndexOutOfBoundsException e) { + return folder; + } + } +} + +@SuppressWarnings("serial") +class DefaultMailFile extends File implements MailFile { + protected transient RandomAccessFile file; + + DefaultMailFile(String name) { + super(name); + } + + @Override + public boolean lock(String mode) { + try { + file = new RandomAccessFile(this, mode); + return true; + } catch (FileNotFoundException fe) { + return false; + } + } + + @Override + public void unlock() { + if (file != null) { + try { + file.close(); + } catch (IOException e) { + // ignore it + } + file = null; + } + } + + @Override + public void touchlock() { + } + + @Override + public FileDescriptor getFD() { + if (file == null) + return null; + try { + return file.getFD(); + } catch (IOException e) { + return null; + } + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/FileInterface.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/FileInterface.java new file mode 100644 index 0000000..a47670e --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/FileInterface.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import java.io.File; +import java.io.FilenameFilter; + +public interface FileInterface { + /** + * Gets the name of the file. This method does not include the + * directory. + * + * @return the file name. + */ + public String getName(); + + /** + * Gets the path of the file. + * + * @return the file path. + */ + public String getPath(); + + /** + * Gets the absolute path of the file. + * + * @return the absolute file path. + */ + public String getAbsolutePath(); + + /** + * Gets the official, canonical path of the File. + * @return canonical path + */ + // XXX - JDK1.1 + // public String getCanonicalPath(); + + /** + * Gets the name of the parent directory. + * + * @return the parent directory, or null if one is not found. + */ + public String getParent(); + + /** + * Returns a boolean indicating whether or not a file exists. + */ + public boolean exists(); + + /** + * Returns a boolean indicating whether or not a writable file + * exists. + */ + public boolean canWrite(); + + /** + * Returns a boolean indicating whether or not a readable file + * exists. + */ + public boolean canRead(); + + /** + * Returns a boolean indicating whether or not a normal file + * exists. + */ + public boolean isFile(); + + /** + * Returns a boolean indicating whether or not a directory file + * exists. + */ + public boolean isDirectory(); + + /** + * Returns a boolean indicating whether the file name is absolute. + */ + public boolean isAbsolute(); + + /** + * Returns the last modification time. The return value should + * only be used to compare modification dates. It is meaningless + * as an absolute time. + */ + public long lastModified(); + + /** + * Returns the length of the file. + */ + public long length(); + + /** + * Creates a directory and returns a boolean indicating the + * success of the creation. Will return false if the directory already + * exists. + */ + public boolean mkdir(); + + /** + * Renames a file and returns a boolean indicating whether + * or not this method was successful. + * + * @param dest the new file name + */ + public boolean renameTo(File dest); + + /** + * Creates all directories in this path. This method + * returns true if the target (deepest) directory was created, + * false if the target directory was not created (e.g., if it + * existed previously). + */ + public boolean mkdirs(); + + /** + * Lists the files in a directory. Works only on directories. + * + * @return an array of file names. This list will include all + * files in the directory except the equivalent of "." and ".." . + */ + public String[] list(); + + /** + * Uses the specified filter to list files in a directory. + * + * @param filter the filter used to select file names + * @return the filter selected files in this directory. + * @see FilenameFilter + */ + public String[] list(FilenameFilter filter); + + /** + * Deletes the specified file. Returns true + * if the file could be deleted. + */ + public boolean delete(); +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/InboxFile.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/InboxFile.java new file mode 100644 index 0000000..6303b68 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/InboxFile.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +public interface InboxFile extends MailFile { + public boolean openLock(String mode); + + public void closeLock(); +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/LineCounter.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/LineCounter.java new file mode 100644 index 0000000..9c5d5a7 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/LineCounter.java @@ -0,0 +1,66 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Count number of lines output. + */ +class LineCounter extends FilterOutputStream { + private int lastb = -1; + protected int lineCount; + + public LineCounter(OutputStream os) { + super(os); + } + + public void write(int b) throws IOException { + // If we have a full line, count it. + if (b == '\r' || (b == '\n' && lastb != '\r')) + lineCount++; + out.write(b); + lastb = b; + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) throws IOException { + for (int i = 0; i < len; i++) { + write(b[off + i]); + } + } + + public int getLineCount() { + return lineCount; + } + + // for testing + public static void main(String[] argv) throws Exception { + int b; + LineCounter os = + new LineCounter(System.out); + while ((b = System.in.read()) >= 0) + os.write(b); + os.flush(); + System.out.println(os.getLineCount()); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/MailFile.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/MailFile.java new file mode 100644 index 0000000..ec6ab76 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/MailFile.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import java.io.FileDescriptor; + +public interface MailFile extends FileInterface { + public boolean lock(String mode); + + public void unlock(); + + public void touchlock(); + + public FileDescriptor getFD(); +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/Mailbox.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/Mailbox.java new file mode 100644 index 0000000..8a66233 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/Mailbox.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +public abstract class Mailbox { + + /** + * Creates a default {@code Mailbox}. + */ + public Mailbox() { + } + + /** + * Return a MailFile object for the specified user's folder. + */ + public abstract MailFile getMailFile(String user, String folder); + + /** + * Return the file name corresponding to a folder with the given name. + */ + public abstract String filename(String user, String folder); +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxFolder.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxFolder.java new file mode 100644 index 0000000..7385729 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxFolder.java @@ -0,0 +1,1207 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.mbox; + +import jakarta.mail.Address; +import jakarta.mail.Flags; +import jakarta.mail.Folder; +import jakarta.mail.FolderNotFoundException; +import jakarta.mail.Message; +import jakarta.mail.MessageRemovedException; +import jakarta.mail.MessagingException; +import jakarta.mail.URLName; +import jakarta.mail.event.ConnectionEvent; +import jakarta.mail.event.FolderEvent; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.InternetHeaders; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.SharedInputStream; +import jakarta.mail.util.SharedByteArrayInputStream; +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.StringTokenizer; +import org.xbib.net.mail.util.LineInputStream; + +/** + * This class represents a mailbox file containing RFC822 style email messages. + * + * @author John Mani + * @author Bill Shannon + */ + +public class MboxFolder extends Folder { + + private final String name; // null => the default folder + private boolean is_inbox = false; + private int total; // total number of messages in mailbox + private volatile boolean opened = false; + private List messages; + private TempFile temp; + private final MboxStore mstore; + private final MailFile folder; + private long file_size; // the size the last time we read or wrote it + private long saved_file_size; // size at the last open, close, or expunge + private boolean special_imap_message; + + private static final boolean homeRelative = + Boolean.getBoolean("mail.mbox.homerelative"); + + /** + * Metadata for each message, to avoid instantiating MboxMessage + * objects for messages we're not going to look at.

+ * + * MboxFolder keeps an array of these objects corresponding to + * each message in the folder. Access to the array elements is + * synchronized, but access to contents of this object is not. + * The metadata stored here is only accessed if the message field + * is null; otherwise the MboxMessage object contains the metadata. + */ + static final class MessageMetadata { + public long start; // offset in temp file of start of this message + // public long end; // offset in temp file of end of this message + public long dataend; // offset of end of message data, <= "end" + public MboxMessage message; // the message itself + public boolean recent; // message is recent? + public boolean deleted; // message is marked deleted? + public boolean imap; // special imap message? + } + + public MboxFolder(MboxStore store, String name) { + super(store); + this.mstore = store; + this.name = name; + + if (name != null && name.equalsIgnoreCase("INBOX")) + is_inbox = true; + + folder = mstore.getMailFile(name == null ? "~" : name); + if (folder.exists()) + saved_file_size = folder.length(); + else + saved_file_size = -1; + } + + @Override + public char getSeparator() { + return File.separatorChar; + } + + @Override + public Folder[] list(String pattern) throws MessagingException { + if (!folder.isDirectory()) + throw new MessagingException("not a directory"); + + if (name == null) + return list(null, pattern, true); + else + return list(name + File.separator, pattern, false); + } + + /* + * Version of list shared by MboxStore and MboxFolder. + */ + protected Folder[] list(String ref, String pattern, boolean fromStore) + throws MessagingException { + if (ref != null && ref.length() == 0) + ref = null; + int i; + String refdir = null; + String realdir = null; + + pattern = canonicalize(ref, pattern); + if ((i = indexOfAny(pattern, "%*")) >= 0) { + refdir = pattern.substring(0, i); + } else { + refdir = pattern; + } + if ((i = refdir.lastIndexOf(File.separatorChar)) >= 0) { + // get rid of anything after directory name + refdir = refdir.substring(0, i + 1); + realdir = mstore.mb.filename(mstore.user, refdir); + } else if (refdir.length() == 0 || refdir.charAt(0) != '~') { + // no separator and doesn't start with "~" => home or cwd + refdir = null; + if (homeRelative) + realdir = mstore.home; + else + realdir = "."; + } else { + realdir = mstore.mb.filename(mstore.user, refdir); + } + List flist = new ArrayList<>(); + listWork(realdir, refdir, pattern, fromStore ? 0 : 1, flist); + if (Match.path("INBOX", pattern, '\0')) + flist.add("INBOX"); + + Folder[] fl = new Folder[flist.size()]; + for (i = 0; i < fl.length; i++) { + fl[i] = createFolder(mstore, flist.get(i)); + } + return fl; + } + + @Override + public String getName() { + if (name == null) + return ""; + else if (is_inbox) + return "INBOX"; + else + return folder.getName(); + } + + @Override + public String getFullName() { + if (name == null) + return ""; + else + return name; + } + + @Override + public Folder getParent() { + if (name == null) + return null; + else if (is_inbox) + return createFolder(mstore, null); + else + // XXX - have to recognize other folders under default folder + return createFolder(mstore, folder.getParent()); + } + + @Override + public boolean exists() { + return folder.exists(); + } + + @Override + public int getType() { + if (folder.isDirectory()) + return HOLDS_FOLDERS; + else + return HOLDS_MESSAGES; + } + + @Override + public Flags getPermanentFlags() { + return MboxStore.permFlags; + } + + @Override + public synchronized boolean hasNewMessages() { + if (folder instanceof UNIXFile) { + UNIXFile f = (UNIXFile) folder; + if (f.length() > 0) { + long atime = f.lastAccessed(); + long mtime = f.lastModified(); +//System.out.println(name + " atime " + atime + " mtime " + mtime); + return atime < mtime; + } + return false; + } + long current_size; + if (folder.exists()) + current_size = folder.length(); + else + current_size = -1; + // if we've never opened the folder, remember the size now + // (will cause us to return false the first time) + if (saved_file_size < 0) + saved_file_size = current_size; + return current_size > saved_file_size; + } + + @Override + public synchronized Folder getFolder(String name) + throws MessagingException { + if (folder.exists() && !folder.isDirectory()) + throw new MessagingException("not a directory"); + return createFolder(mstore, + (this.name == null ? "~" : this.name) + File.separator + name); + } + + @Override + public synchronized boolean create(int type) throws MessagingException { + switch (type) { + case HOLDS_FOLDERS: + if (!folder.mkdirs()) { + return false; + } + break; + + case HOLDS_MESSAGES: + if (folder.exists()) { + return false; + } + try { + (new FileOutputStream((File) folder)).close(); + } catch (FileNotFoundException fe) { + File parent = new File(folder.getParent()); + if (!parent.mkdirs()) + throw new + MessagingException("can't create folder: " + name); + try { + (new FileOutputStream((File) folder)).close(); + } catch (IOException ex3) { + throw new + MessagingException("can't create folder: " + name, ex3); + } + } catch (IOException e) { + throw new + MessagingException("can't create folder: " + name, e); + } + break; + + default: + throw new MessagingException("type not supported"); + } + notifyFolderListeners(FolderEvent.CREATED); + return true; + } + + @Override + public synchronized boolean delete(boolean recurse) + throws MessagingException { + checkClosed(); + if (name == null) + throw new MessagingException("can't delete default folder"); + boolean ret = true; + if (recurse && folder.isDirectory()) + ret = delete(new File(folder.getPath())); + if (ret && folder.delete()) { + notifyFolderListeners(FolderEvent.DELETED); + return true; + } + return false; + } + + /** + * Recursively delete the specified file/directory. + */ + private boolean delete(File f) { + File[] files = f.listFiles(); + boolean ret = true; + for (int i = 0; ret && i < files.length; i++) { + if (files[i].isDirectory()) + ret = delete(files[i]); + else + ret = files[i].delete(); + } + return ret; + } + + @Override + public synchronized boolean renameTo(Folder f) + throws MessagingException { + checkClosed(); + if (name == null) + throw new MessagingException("can't rename default folder"); + if (!(f instanceof MboxFolder)) + throw new MessagingException("can't rename to: " + f.getName()); + String newname = ((MboxFolder) f).folder.getPath(); + if (folder.renameTo(new File(newname))) { + notifyFolderRenamedListeners(f); + return true; + } + return false; + } + + /* Ensure the folder is open */ + 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"); + } + + /* + * 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 check to see if new messages + * have arrived. + */ + private 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 check if there are any new messages. + getMessageCount(); + + if (msgno > total) // Still out of range ? Throw up ... + throw new IndexOutOfBoundsException(msgno + " > " + total); + } + + /* 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"); + } + + @Override + public boolean isOpen() { + return opened; + } + + /* + * Open the folder in the specified mode. + */ + @Override + @SuppressWarnings("fallthrough") + public synchronized void open(int mode) throws MessagingException { + if (opened) + throw new IllegalStateException("Folder is already Open"); + + if (!folder.exists()) + throw new FolderNotFoundException(this, "Folder doesn't exist: " + + folder.getPath()); + this.mode = mode; + switch (mode) { + case READ_WRITE: + default: + if (!folder.canWrite()) + throw new MessagingException("Open Failure, can't write: " + + folder.getPath()); + // fall through... + + case READ_ONLY: + if (!folder.canRead()) + throw new MessagingException("Open Failure, can't read: " + + folder.getPath()); + break; + } + + if (is_inbox && folder instanceof InboxFile) { + InboxFile inf = (InboxFile) folder; + if (!inf.openLock(mode == READ_WRITE ? "rw" : "r")) + throw new MessagingException("Failed to lock INBOX"); + } + if (!folder.lock("r")) + throw new MessagingException("Failed to lock folder: " + name); + messages = new ArrayList<>(); + total = 0; + Message[] msglist = null; + try { + temp = new TempFile(null); + saved_file_size = folder.length(); + msglist = load(0L, false); + } catch (IOException e) { + throw new MessagingException("IOException", e); + } finally { + folder.unlock(); + } + notifyConnectionListeners(ConnectionEvent.OPENED); + if (msglist != null) + notifyMessageAddedListeners(msglist); + opened = true; // now really opened + } + + @Override + public synchronized void close(boolean expunge) throws MessagingException { + checkOpen(); + + try { + if (mode == READ_WRITE) { + try { + writeFolder(true, expunge); + } catch (IOException e) { + throw new MessagingException("I/O Exception", e); + } + } + messages = null; + } finally { + opened = false; + if (is_inbox && folder instanceof InboxFile) { + InboxFile inf = (InboxFile) folder; + inf.closeLock(); + } + temp.close(); + temp = null; + notifyConnectionListeners(ConnectionEvent.CLOSED); + } + } + + /** + * Re-write the folder with the current contents of the messages. + * If closing is true, turn off the RECENT flag. If expunge is + * true, don't write out deleted messages (only used from close() + * when the message cache won't be accessed again). + * + * Return the number of messages written. + */ + protected int writeFolder(boolean closing, boolean expunge) + throws IOException, MessagingException { + + /* + * First, see if there have been any changes. + */ + int modified = 0, deleted = 0, recent = 0; + for (int msgno = 1; msgno <= total; msgno++) { + MessageMetadata md = messages.get(messageIndexOf(msgno)); + MboxMessage msg = md.message; + if (msg != null) { + Flags flags = msg.getFlags(); + if (msg.isModified() || !msg.origFlags.equals(flags)) + modified++; + if (flags.contains(Flags.Flag.DELETED)) + deleted++; + if (flags.contains(Flags.Flag.RECENT)) + recent++; + } else { + if (md.deleted) + deleted++; + if (md.recent) + recent++; + } + } + if ((!closing || recent == 0) && (!expunge || deleted == 0) && + modified == 0) + return 0; + + /* + * Have to save any new mail that's been appended to the + * folder since we last loaded it. + */ + if (!folder.lock("rw")) + throw new MessagingException("Failed to lock folder: " + name); + int oldtotal = total; // XXX + Message[] msglist = null; + if (folder.length() != file_size) + msglist = load(file_size, !closing); + // don't use the folder's FD, need to re-open in order to trunc the file + OutputStream os = + new BufferedOutputStream(new FileOutputStream((File) folder)); + int wr = 0; + boolean keep = true; + try { + if (special_imap_message) + appendStream(getMessageStream(0), os); + for (int msgno = 1; msgno <= total; msgno++) { + MessageMetadata md = messages.get(messageIndexOf(msgno)); + MboxMessage msg = md.message; + if (msg != null) { + if (expunge && msg.isSet(Flags.Flag.DELETED)) + continue; // skip it; + if (closing && msgno <= oldtotal && + msg.isSet(Flags.Flag.RECENT)) + msg.setFlag(Flags.Flag.RECENT, false); + writeMboxMessage(msg, os); + } else { + if (expunge && md.deleted) + continue; // skip it; + if (closing && msgno <= oldtotal && md.recent) { + // have to instantiate message so that we can + // clear the recent flag + msg = (MboxMessage) getMessage(msgno); + msg.setFlag(Flags.Flag.RECENT, false); + writeMboxMessage(msg, os); + } else { + appendStream(getMessageStream(msgno), os); + } + } + folder.touchlock(); + wr++; + } + // If no messages in the mailbox, and we're closing, + // maybe we should remove the mailbox. + if (wr == 0 && closing) { + String skeep = ((MboxStore) store).getSession(). + getProperty("mail.mbox.deleteEmpty"); + if (skeep != null && skeep.equalsIgnoreCase("true")) + keep = false; + } + } catch (IOException | MessagingException e) { + throw e; + } catch (Exception e) { + e.printStackTrace(); + throw new MessagingException("unexpected exception " + e); + } finally { + // close the folder, flushing out the data + try { + os.close(); + file_size = saved_file_size = folder.length(); + if (!keep) { + folder.delete(); + file_size = 0; + } + } catch (IOException ex) { + } + + if (keep) { + // make sure the access time is greater than the mod time + // XXX - would be nice to have utime() + try { + Thread.sleep(1000); // sleep for a second + } catch (InterruptedException ex) { + } + InputStream is = null; + try { + is = new FileInputStream((File) folder); + is.read(); // read a byte + } catch (IOException ex) { + } // ignore errors + try { + if (is != null) + is.close(); + is = null; + } catch (IOException ex) { + } // ignore errors + } + + folder.unlock(); + if (msglist != null) + notifyMessageAddedListeners(msglist); + } + return wr; + } + + /** + * Append the input stream to the output stream, closing the + * input stream when done. + */ + private static final void appendStream(InputStream is, OutputStream os) + throws IOException { + try { + byte[] buf = new byte[64 * 1024]; + int len; + while ((len = is.read(buf)) > 0) + os.write(buf, 0, len); + } finally { + is.close(); + } + } + + /** + * Write a MimeMessage to the specified OutputStream in a + * format suitable for a UNIX mailbox, i.e., including a correct + * Content-Length header and with the local platform's line + * terminating convention.

+ * + * If the message is really a MboxMessage, use its writeToFile + * method, which has access to the UNIX From line. Otherwise, do + * all the work here, creating an appropriate UNIX From line. + */ + public static void writeMboxMessage(MimeMessage msg, OutputStream os) + throws IOException, MessagingException { + try { + if (msg instanceof MboxMessage) { + ((MboxMessage) msg).writeToFile(os); + } else { + // XXX - modify the message to preserve the flags in headers + MboxMessage.setHeadersFromFlags(msg); + ContentLengthCounter cos = new ContentLengthCounter(); + NewlineOutputStream nos = new NewlineOutputStream(cos); + msg.writeTo(nos); + nos.flush(); + os = new NewlineOutputStream(os, true); + os = new ContentLengthUpdater(os, cos.getSize()); + PrintStream pos = new PrintStream(os, false, "iso-8859-1"); + pos.println(getUnixFrom(msg)); + msg.writeTo(pos); + pos.flush(); + } + } catch (MessagingException | IOException me) { + throw me; + } + } + + /** + * Construct an appropriately formatted UNIX From line using + * the sender address and the date in the message. + */ + protected static String getUnixFrom(MimeMessage msg) { + Address[] afrom; + String from; + Date ddate; + String date; + try { + if ((afrom = msg.getFrom()) == null || + !(afrom[0] instanceof InternetAddress) || + (from = ((InternetAddress) afrom[0]).getAddress()) == null) + from = "UNKNOWN"; + if ((ddate = msg.getReceivedDate()) == null || + (ddate = msg.getSentDate()) == null) + ddate = new Date(); + } catch (MessagingException e) { + from = "UNKNOWN"; + ddate = new Date(); + } + date = ddate.toString(); + // date is of the form "Sat Aug 12 02:30:00 PDT 1995" + // need to strip out the timezone + return "From " + from + " " + + date.substring(0, 20) + date.substring(24); + } + + @Override + public synchronized int getMessageCount() throws MessagingException { + if (!opened) + return -1; + + boolean locked = false; + Message[] msglist = null; + try { + if (folder.length() != file_size) { + if (!folder.lock("r")) + throw new MessagingException("Failed to lock folder: " + + name); + locked = true; + msglist = load(file_size, true); + } + } catch (IOException e) { + throw new MessagingException("I/O Exception", e); + } finally { + if (locked) { + folder.unlock(); + if (msglist != null) + notifyMessageAddedListeners(msglist); + } + } + return total; + } + + /** + * Get the specified message. Note that messages are numbered + * from 1. + */ + @Override + public synchronized Message getMessage(int msgno) + throws MessagingException { + checkReadable(); + checkRange(msgno); + + MessageMetadata md = messages.get(messageIndexOf(msgno)); + MboxMessage m = md.message; + if (m == null) { + InputStream is = getMessageStream(msgno); + try { + m = loadMessage(is, msgno, mode == READ_WRITE); + } catch (IOException ex) { + MessagingException mex = + new MessageRemovedException("mbox message gone", ex); + throw mex; + } + md.message = m; + } + return m; + } + + private final int messageIndexOf(int msgno) { + return special_imap_message ? msgno : msgno - 1; + } + + private InputStream getMessageStream(int msgno) { + int index = messageIndexOf(msgno); + MessageMetadata md = messages.get(index); + return temp.newStream(md.start, md.dataend); + } + + @Override + public synchronized void appendMessages(Message[] msgs) + throws MessagingException { + if (!folder.lock("rw")) + throw new MessagingException("Failed to lock folder: " + name); + + OutputStream os = null; + boolean err = false; + try { + os = new BufferedOutputStream( + new FileOutputStream(((File) folder).getPath(), true)); + // XXX - should use getAbsolutePath()? + for (int i = 0; i < msgs.length; i++) { + if (msgs[i] instanceof MimeMessage) { + writeMboxMessage((MimeMessage) msgs[i], os); + } else { + err = true; + continue; + } + folder.touchlock(); + } + } catch (IOException e) { + throw new MessagingException("I/O Exception", e); + } catch (MessagingException e) { + throw e; + } catch (Exception e) { + e.printStackTrace(); + throw new MessagingException("unexpected exception " + e); + } finally { + if (os != null) + try { + os.close(); + } catch (IOException e) { + // ignored + } + folder.unlock(); + } + if (opened) + getMessageCount(); // loads new messages as a side effect + if (err) + throw new MessagingException("Can't append non-Mime message"); + } + + @Override + public synchronized Message[] expunge() throws MessagingException { + checkWritable(); + + /* + * First, write out the folder to make sure we have permission, + * disk space, etc. + */ + int wr = total; // number of messages written out + try { + wr = writeFolder(false, true); + } catch (IOException e) { + throw new MessagingException("expunge failed", e); + } + if (wr == total) // wrote them all => nothing expunged + return new Message[0]; + + /* + * Now, actually get rid of the expunged messages. + */ + int del = 0; + Message[] msglist = new Message[total - wr]; + int msgno = 1; + while (msgno <= total) { + MessageMetadata md = messages.get(messageIndexOf(msgno)); + MboxMessage msg = md.message; + if (msg != null) { + if (msg.isSet(Flags.Flag.DELETED)) { + msg.setExpunged(true); + msglist[del] = msg; + del++; + messages.remove(messageIndexOf(msgno)); + total--; + } else { + msg.setMessageNumber(msgno); // update message number + msgno++; + } + } else { + if (md.deleted) { + // have to instantiate it for the notification + msg = (MboxMessage) getMessage(msgno); + msg.setExpunged(true); + msglist[del] = msg; + del++; + messages.remove(messageIndexOf(msgno)); + total--; + } else { + msgno++; + } + } + } + if (del != msglist.length) // this is really an assert + throw new MessagingException("expunge delete count wrong"); + notifyMessageRemovedListeners(true, msglist); + return msglist; + } + + /* + * Load more messages from the folder starting at the specified offset. + */ + private Message[] load(long offset, boolean notify) + throws MessagingException, IOException { + int oldtotal = total; + MessageLoader loader = new MessageLoader(temp); + int loaded = loader.load(folder.getFD(), offset, messages); + total += loaded; + file_size = folder.length(); + + if (offset == 0 && loaded > 0) { + /* + * If the first message is the special message that the + * IMAP server adds to the mailbox, remember that we've + * seen it so it won't be shown to the user. + */ + MessageMetadata md = messages.get(0); + if (md.imap) { + special_imap_message = true; + total--; + } + } + if (notify) { + Message[] msglist = new Message[total - oldtotal]; + for (int i = oldtotal, j = 0; i < total; i++, j++) + msglist[j] = getMessage(i + 1); + return msglist; + } else + return null; + } + + /** + * Parse the input stream and return an appropriate message object. + * The InputStream must be a SharedInputStream. + */ + private MboxMessage loadMessage(InputStream is, int msgno, + boolean writable) throws MessagingException, IOException { + LineInputStream in = new LineInputStream(is); + + /* + * Read lines until a UNIX From line, + * skipping blank lines. + */ + String line; + String unix_from = null; + while ((line = in.readLine()) != null) { + if (line.trim().length() == 0) + continue; + if (line.startsWith("From ")) { + /* + * A UNIX From line looks like: + * From address Day Mon DD HH:MM:SS YYYY + */ + unix_from = line; + int i; + // find the space after the address, before the date + i = unix_from.indexOf(' ', 5); + if (i < 0) + continue; // not a valid UNIX From line + break; + } + throw new MessagingException("Garbage in mailbox: " + line); + } + + if (unix_from == null) + throw new EOFException("end of mailbox"); + + /* + * Now load the RFC822 headers into an InternetHeaders object. + */ + InternetHeaders hdrs = new InternetHeaders(is); + + // the rest is the message content + SharedInputStream sis = (SharedInputStream) is; + InputStream stream = sis.newStream(sis.getPosition(), -1); + return new MboxMessage(this, hdrs, stream, msgno, unix_from, writable); + } + + /* + * Only here to make accessible to MboxMessage. + */ + @Override + protected void notifyMessageChangedListeners(int type, Message m) { + super.notifyMessageChangedListeners(type, m); + } + + + /** + * this is an exact duplicate of the Folder.getURL except it doesn't + * add a beginning '/' to the URLName. + */ + @Override + public URLName getURLName() { + // XXX - note: this should not be done this way with the + // new jakarta.mail apis. + + URLName storeURL = getStore().getURLName(); + if (name == null) + return storeURL; + + char separator = getSeparator(); + String fullname = getFullName(); + StringBuilder encodedName = new StringBuilder(); + + // We need to encode each of the folder's names, and replace + // the store's separator char with the URL char '/'. + StringTokenizer tok = new StringTokenizer( + fullname, Character.toString(separator), true); + + while (tok.hasMoreTokens()) { + String s = tok.nextToken(); + if (s.charAt(0) == separator) + encodedName.append("/"); + else + // XXX - should encode, but since there's no decoder... + //encodedName.append(java.net.URLEncoder.encode(s)); + encodedName.append(s); + } + + return new URLName(storeURL.getProtocol(), storeURL.getHost(), + storeURL.getPort(), encodedName.toString(), + storeURL.getUsername(), + null /* no password */); + } + + /** + * Create an MboxFolder object, or a subclass thereof. + * Can be overridden by subclasses of MboxFolder so that + * the appropriate subclass is created by the list method. + */ + protected Folder createFolder(MboxStore store, String name) { + return new MboxFolder(store, name); + } + + /* + * Support routines for list(). + */ + + /** + * Return a canonicalized pattern given a reference name and a pattern. + */ + private static String canonicalize(String ref, String pat) { + if (ref == null) + return pat; + try { + if (pat.length() == 0) { + return ref; + } else if (pat.charAt(0) == File.separatorChar) { + return ref.substring(0, ref.indexOf(File.separatorChar)) + pat; + } else { + return ref + pat; + } + } catch (StringIndexOutOfBoundsException e) { + return pat; + } + } + + /** + * Return the first index of any of the characters in "any" in "s", + * or -1 if none are found. + * + * This should be a method on String. + */ + private static int indexOfAny(String s, String any) { + try { + int len = s.length(); + for (int i = 0; i < len; i++) { + if (any.indexOf(s.charAt(i)) >= 0) + return i; + } + return -1; + } catch (StringIndexOutOfBoundsException e) { + return -1; + } + } + + /** + * The recursive part of generating the list of mailboxes. + * realdir is the full pathname to the directory to search. + * dir is the name the user uses, often a relative name that's + * relative to the user's home directory. dir (if not null) always + * has a trailing file separator character. + * + * @param realdir real pathname of directory to start looking in + * @param dir user's name for realdir + * @param pat pattern to match against + * @param level level of the directory hierarchy we're in + * @param flist list to which to add folder names that match + */ + // Derived from the c-client listWork() function. + private void listWork(String realdir, String dir, String pat, + int level, List flist) { + String[] sl; + File fdir = new File(realdir); + sl = fdir.list(); + + if (level == 0 && dir != null && + Match.path(dir, pat, File.separatorChar)) + flist.add(dir); + + if (sl == null) + return; // nothing return, we're done + + if (realdir.charAt(realdir.length() - 1) != File.separatorChar) + realdir += File.separator; + + for (int i = 0; i < sl.length; i++) { + if (sl[i].charAt(0) == '.') + continue; // ignore all "dot" files for now + String md = realdir + sl[i]; + File mf = new File(md); + if (!mf.exists()) + continue; + String name; + if (dir != null) + name = dir + sl[i]; + else + name = sl[i]; + if (mf.isDirectory()) { + if (Match.path(name, pat, File.separatorChar)) { + flist.add(name); + name += File.separator; + } else { + name += File.separator; + if (Match.path(name, pat, File.separatorChar)) + flist.add(name); + } + if (Match.dir(name, pat, File.separatorChar)) + listWork(md, name, pat, level + 1, flist); + } else { + if (Match.path(name, pat, File.separatorChar)) + flist.add(name); + } + } + } +} + +/** + * Pattern matching support class for list(). + * Should probably be more public. + */ +// Translated from the c-client functions pmatch_full() and dmatch(). +class Match { + /** + * Pathname pattern match + * + * @param s base string + * @param pat pattern string + * @param delim delimiter character + * @return true if base matches pattern + */ + static public boolean path(String s, String pat, char delim) { + try { + return path(s, 0, s.length(), pat, 0, pat.length(), delim); + } catch (StringIndexOutOfBoundsException e) { + return false; + } + } + + static private boolean path(String s, int s_index, int s_len, + String pat, int p_index, int p_len, char delim) + throws StringIndexOutOfBoundsException { + + while (p_index < p_len) { + char c = pat.charAt(p_index); + switch (c) { + case '%': + if (++p_index >= p_len) // % at end of pattern + // ok if no delimiters + return delim == 0 || s.indexOf(delim, s_index) < 0; + // scan remainder until delimiter + do { + if (path(s, s_index, s_len, pat, p_index, p_len, delim)) + return true; + } while (s.charAt(s_index) != delim && ++s_index < s_len); + // ran into a delimiter or ran out of string without a match + return false; + + case '*': + if (++p_index >= p_len) // end of pattern? + return true; // unconditional match + do { + if (path(s, s_index, s_len, pat, p_index, p_len, delim)) + return true; + } while (++s_index < s_len); + // ran out of string without a match + return false; + + default: + // if ran out of string or no match, fail + if (s_index >= s_len || c != s.charAt(s_index)) + return false; + + // try the next string and pattern characters + s_index++; + p_index++; + } + } + return s_index >= s_len; + } + + /** + * Directory pattern match + * + * @param s base string + * @param pat pattern string + * @return true if base is a matching directory of pattern + */ + static public boolean dir(String s, String pat, char delim) { + try { + return dir(s, 0, s.length(), pat, 0, pat.length(), delim); + } catch (StringIndexOutOfBoundsException e) { + return false; + } + } + + static private boolean dir(String s, int s_index, int s_len, + String pat, int p_index, int p_len, char delim) + throws StringIndexOutOfBoundsException { + + while (p_index < p_len) { + char c = pat.charAt(p_index); + switch (c) { + case '%': + if (s_index >= s_len) // end of base? + return true; // subset match + if (++p_index >= p_len) // % at end of pattern? + return false; // no inferiors permitted + do { + if (dir(s, s_index, s_len, pat, p_index, p_len, delim)) + return true; + } while (s.charAt(s_index) != delim && ++s_index < s_len); + + if (s_index + 1 == s_len) // s ends with a delimiter + return true; // must be a subset of pattern + return dir(s, s_index, s_len, pat, p_index, p_len, delim); + + case '*': + return true; // unconditional match + + default: + if (s_index >= s_len) // end of base? + return c == delim; // matched if at delimiter + + if (c != s.charAt(s_index)) + return false; + + // try the next string and pattern characters + s_index++; + p_index++; + } + } + return s_index >= s_len; + } +} + +/** + * 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. + */ +class SharedByteArrayOutputStream extends ByteArrayOutputStream { + public SharedByteArrayOutputStream(int size) { + super(size); + } + + public InputStream toStream() { + return new SharedByteArrayInputStream(buf, 0, count); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxMessage.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxMessage.java new file mode 100644 index 0000000..bd6bc70 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxMessage.java @@ -0,0 +1,547 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.mbox; + +import jakarta.activation.DataHandler; +import jakarta.mail.Address; +import jakarta.mail.Flags; +import jakarta.mail.MessageRemovedException; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.event.MessageChangedEvent; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.InternetHeaders; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimePartDataSource; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.StringTokenizer; +import org.xbib.net.mail.util.LineInputStream; + +/** + * This class represents an RFC822 style email message that resides in a file. + * + * @author Bill Shannon + */ + +public class MboxMessage extends MimeMessage { + + boolean writable = false; + // original msg flags, used by MboxFolder to detect modification + Flags origFlags; + /* + * A UNIX From line looks like: + * From address Day Mon DD HH:MM:SS YYYY + */ + String unix_from; + InternetAddress unix_from_user; + Date rcvDate; + int lineCount = -1; + private static OutputStream nullOutputStream = new OutputStream() { + @Override + public void write(int b) { + } + + @Override + public void write(byte[] b, int off, int len) { + } + }; + + /** + * Construct an MboxMessage from the InputStream. + */ + @SuppressWarnings("this-escape") + public MboxMessage(Session session, InputStream is) + throws MessagingException, IOException { + super(session); + BufferedInputStream bis; + if (is instanceof BufferedInputStream) + bis = (BufferedInputStream) is; + else + bis = new BufferedInputStream(is); + LineInputStream dis = new LineInputStream(bis); + bis.mark(1024); + String line = dis.readLine(); + if (line != null && line.startsWith("From ")) + this.unix_from = line; + else + bis.reset(); + parse(bis); + saved = true; + } + + /** + * Construct an MboxMessage using the given InternetHeaders object + * and content from an InputStream. + */ + @SuppressWarnings("this-escape") + public MboxMessage(MboxFolder folder, InternetHeaders hdrs, InputStream is, + int msgno, String unix_from, boolean writable) + throws MessagingException { + super(folder, hdrs, null, msgno); + setFlagsFromHeaders(); + origFlags = getFlags(); + this.unix_from = unix_from; + this.writable = writable; + this.contentStream = is; + } + + /** + * Returns the "From" attribute. The "From" attribute contains + * the identity of the person(s) who wished this message to + * be sent.

+ * + * If our superclass doesn't have a value, we return the address + * from the UNIX From line. + * + * @return array of Address objects + */ + public Address[] getFrom() throws MessagingException { + Address[] ret = super.getFrom(); + if (ret == null) { + InternetAddress ia = getUnixFrom(); + if (ia != null) + ret = new InternetAddress[]{ia}; + } + return ret; + } + + /** + * Returns the address from the UNIX "From" line. + * + * @return UNIX From address + */ + public synchronized InternetAddress getUnixFrom() + throws MessagingException { + if (unix_from_user == null && unix_from != null) { + int i; + // find the space after the address, before the date + i = unix_from.indexOf(' ', 5); + if (i > 5) { + try { + unix_from_user = new InternetAddress(unix_from.substring(5, i)); + } catch (AddressException e) { + // ignore it + } + } + } + return unix_from_user; + } + + private String getUnixFromLine() { + if (unix_from != null) + return unix_from; + String from = "unknown"; + try { + Address[] froma = getFrom(); + if (froma != null && froma.length > 0 && + froma[0] instanceof InternetAddress) + from = ((InternetAddress) froma[0]).getAddress(); + } catch (MessagingException ex) { + } + Date d = null; + try { + d = getSentDate(); + } catch (MessagingException ex) { + // ignore + } + if (d == null) + d = new Date(); + // From shannon Mon Jun 10 12:06:52 2002 + SimpleDateFormat fmt = new SimpleDateFormat("EEE LLL dd HH:mm:ss yyyy"); + return "From " + from + " " + fmt.format(d); + } + + /** + * Get the date this message was received, from the UNIX From line. + * + * @return the date this message was received + */ + @SuppressWarnings("deprecation") // for Date constructor + public Date getReceivedDate() throws MessagingException { + if (rcvDate == null && unix_from != null) { + int i; + // find the space after the address, before the date + i = unix_from.indexOf(' ', 5); + if (i > 5) { + try { + rcvDate = new Date(unix_from.substring(i)); + } catch (IllegalArgumentException iae) { + // ignore it + } + } + } + return rcvDate == null ? null : new Date(rcvDate.getTime()); + } + + /** + * Return the number of lines for the content of this message. + * Return -1 if this number cannot be determined.

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

+ * + * This implementation returns -1. + * + * @return number of lines in the content. + * @exception MessagingException + */ + public int getLineCount() throws MessagingException { + if (lineCount < 0 && isMimeType("text/plain")) { + LineCounter lc = null; + // writeTo will set the SEEN flag, remember the original state + boolean seen = isSet(Flags.Flag.SEEN); + try { + lc = new LineCounter(nullOutputStream); + getDataHandler().writeTo(lc); + lineCount = lc.getLineCount(); + } catch (IOException ex) { + // ignore it, can't happen + } finally { + try { + if (lc != null) + lc.close(); + } catch (IOException ex) { + // can't happen + } + } + if (!seen) + setFlag(Flags.Flag.SEEN, false); + } + return lineCount; + } + + /** + * 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 + */ + public void setFlags(Flags newFlags, boolean set) + throws MessagingException { + Flags oldFlags = (Flags) flags.clone(); + super.setFlags(newFlags, set); + if (!flags.equals(oldFlags)) { + setHeadersFromFlags(this); + if (folder != null) + ((MboxFolder) folder).notifyMessageChangedListeners( + MessageChangedEvent.FLAGS_CHANGED, this); + } + } + + /** + * Return the content type, mapping from SunV3 types to MIME types + * as necessary. + */ + public String getContentType() throws MessagingException { + String ct = super.getContentType(); + if (ct.indexOf('/') < 0) + ct = SunV3BodyPart.MimeV3Map.toMime(ct); + return ct; + } + + /** + * Produce the raw bytes of the content. This method is used during + * parsing, to create a DataHandler object for the content. Subclasses + * that can provide a separate input stream for just the message + * content might want to override this method.

+ * + * This implementation just returns a ByteArrayInputStream constructed + * out of the content byte array. + * + * @see #content + */ + protected InputStream getContentStream() throws MessagingException { + if (folder != null) + ((MboxFolder) folder).checkOpen(); + if (isExpunged()) + throw new MessageRemovedException("mbox message expunged"); + if (!isSet(Flags.Flag.SEEN)) + setFlag(Flags.Flag.SEEN, true); + return super.getContentStream(); + } + + /** + * Return a DataHandler for this Message's content. + * If this is a SunV3 multipart message, handle it specially. + * + * @exception MessagingException + */ + public synchronized DataHandler getDataHandler() + throws MessagingException { + if (dh == null) { + // XXX - Following is a kludge to avoid having to register + // the "multipart/x-sun-attachment" data type with the JAF. + String ct = getContentType(); + if (ct.equalsIgnoreCase("multipart/x-sun-attachment")) + dh = new DataHandler( + new SunV3Multipart(new MimePartDataSource(this)), ct); + else + return super.getDataHandler(); // will set "dh" + } + return dh; + } + + // here only to allow package private access from MboxFolder + protected void setMessageNumber(int msgno) { + super.setMessageNumber(msgno); + } + + // here to synchronize access to expunged field + public synchronized boolean isExpunged() { + return super.isExpunged(); + } + + // here to synchronize and to allow access from MboxFolder + protected synchronized void setExpunged(boolean expunged) { + super.setExpunged(expunged); + } + + // XXX - We assume that only body parts that are part of a SunV3 + // multipart will use the SunV3 headers (X-Sun-Content-Length, + // X-Sun-Content-Lines, X-Sun-Data-Type, X-Sun-Encoding-Info, + // X-Sun-Data-Description, X-Sun-Data-Name) so we don't handle + // them here. + + /** + * Set the flags for this message based on the Status, + * X-Status, and X-Dt-Delete-Time headers. + * + * SIMS 2.0: + * "X-Status: DFAT", deleted, flagged, answered, draft. + * Unset flags represented as "$". + * User flags not supported. + * + * University of Washington IMAP server: + * "X-Status: DFAT", deleted, flagged, answered, draft. + * Unset flags not present. + * "X-Keywords: userflag1 userflag2" + */ + private synchronized void setFlagsFromHeaders() { + flags = new Flags(Flags.Flag.RECENT); + try { + String s = getHeader("Status", null); + if (s != null) { + if (s.indexOf('R') >= 0) + flags.add(Flags.Flag.SEEN); + if (s.indexOf('O') >= 0) + flags.remove(Flags.Flag.RECENT); + } + s = getHeader("X-Dt-Delete-Time", null); // set by dtmail + if (s != null) + flags.add(Flags.Flag.DELETED); + s = getHeader("X-Status", null); // set by IMAP server + if (s != null) { + if (s.indexOf('D') >= 0) + flags.add(Flags.Flag.DELETED); + if (s.indexOf('F') >= 0) + flags.add(Flags.Flag.FLAGGED); + if (s.indexOf('A') >= 0) + flags.add(Flags.Flag.ANSWERED); + if (s.indexOf('T') >= 0) + flags.add(Flags.Flag.DRAFT); + } + s = getHeader("X-Keywords", null); // set by IMAP server + if (s != null) { + StringTokenizer st = new StringTokenizer(s); + while (st.hasMoreTokens()) + flags.add(st.nextToken()); + } + } catch (MessagingException e) { + // ignore it + } + } + + /** + * Set the various header fields that represent the message flags. + */ + static void setHeadersFromFlags(MimeMessage msg) { + try { + Flags flags = msg.getFlags(); + StringBuilder status = new StringBuilder(); + if (flags.contains(Flags.Flag.SEEN)) + status.append('R'); + if (!flags.contains(Flags.Flag.RECENT)) + status.append('O'); + if (status.length() > 0) + msg.setHeader("Status", status.toString()); + else + msg.removeHeader("Status"); + + boolean sims = false; + String s = msg.getHeader("X-Status", null); + // is it a SIMS 2.0 format X-Status header? + sims = s != null && s.length() == 4 && s.indexOf('$') >= 0; + status.setLength(0); + if (flags.contains(Flags.Flag.DELETED)) + status.append('D'); + else if (sims) + status.append('$'); + if (flags.contains(Flags.Flag.FLAGGED)) + status.append('F'); + else if (sims) + status.append('$'); + if (flags.contains(Flags.Flag.ANSWERED)) + status.append('A'); + else if (sims) + status.append('$'); + if (flags.contains(Flags.Flag.DRAFT)) + status.append('T'); + else if (sims) + status.append('$'); + if (status.length() > 0) + msg.setHeader("X-Status", status.toString()); + else + msg.removeHeader("X-Status"); + + String[] userFlags = flags.getUserFlags(); + if (userFlags.length > 0) { + status.setLength(0); + for (int i = 0; i < userFlags.length; i++) + status.append(userFlags[i]).append(' '); + status.setLength(status.length() - 1); // smash trailing space + msg.setHeader("X-Keywords", status.toString()); + } + if (flags.contains(Flags.Flag.DELETED)) { + s = msg.getHeader("X-Dt-Delete-Time", null); + if (s == null) + // XXX - should be time + msg.setHeader("X-Dt-Delete-Time", "1"); + } + } catch (MessagingException e) { + // ignore it + } + } + + protected void updateHeaders() throws MessagingException { + super.updateHeaders(); + setHeadersFromFlags(this); + } + + /** + * Save any changes made to this message. + */ + public void saveChanges() throws MessagingException { + if (folder != null) + ((MboxFolder) folder).checkOpen(); + if (isExpunged()) + throw new MessageRemovedException("mbox message expunged"); + if (!writable) + throw new MessagingException("Message is read-only"); + + super.saveChanges(); + + try { + /* + * Count the size of the body, in order to set the Content-Length + * header. (Should we only do this to update an existing + * Content-Length header?) + * XXX - We could cache the content bytes here, for use later + * in writeTo. + */ + ContentLengthCounter cos = new ContentLengthCounter(); + OutputStream os = new NewlineOutputStream(cos); + super.writeTo(os); + os.flush(); + setHeader("Content-Length", String.valueOf(cos.getSize())); + // setContentSize((int)cos.getSize()); + } catch (MessagingException e) { + throw e; + } catch (Exception e) { + throw new MessagingException("unexpected exception " + e); + } + } + + /** + * Expose modified flag to MboxFolder. + */ + boolean isModified() { + return modified; + } + + /** + * Put out a byte stream suitable for saving to a file. + * XXX - ultimately implement "ignore headers" here? + */ + public void writeToFile(OutputStream os) throws IOException { + try { + if (getHeader("Content-Length") == null) { + /* + * Count the size of the body, in order to set the + * Content-Length header. + */ + ContentLengthCounter cos = new ContentLengthCounter(); + OutputStream oos = new NewlineOutputStream(cos); + super.writeTo(oos, null); + oos.flush(); + setHeader("Content-Length", String.valueOf(cos.getSize())); + // setContentSize((int)cos.getSize()); + } + + os = new NewlineOutputStream(os, true); + PrintStream pos = new PrintStream(os, false, "iso-8859-1"); + + pos.println(getUnixFromLine()); + super.writeTo(pos, null); + pos.flush(); + } catch (MessagingException e) { + throw new IOException("unexpected exception " + e); + } + } + + public void writeTo(OutputStream os, String[] ignoreList) + throws IOException, MessagingException { + // set the SEEN flag now, which will normally be set by + // getContentStream, so it will show up in our headers + if (!isSet(Flags.Flag.SEEN)) + setFlag(Flags.Flag.SEEN, true); + super.writeTo(os, ignoreList); + } + + /** + * Interpose on superclass method to make sure folder is still open + * and message hasn't been expunged. + */ + public String[] getHeader(String name) + throws MessagingException { + if (folder != null) + ((MboxFolder) folder).checkOpen(); + if (isExpunged()) + throw new MessageRemovedException("mbox message expunged"); + return super.getHeader(name); + } + + /** + * Interpose on superclass method to make sure folder is still open + * and message hasn't been expunged. + */ + public String getHeader(String name, String delimiter) + throws MessagingException { + if (folder != null) + ((MboxFolder) folder).checkOpen(); + if (isExpunged()) + throw new MessageRemovedException("mbox message expunged"); + return super.getHeader(name, delimiter); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxProvider.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxProvider.java new file mode 100644 index 0000000..54185f3 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import jakarta.mail.Provider; + +/** + * The Mbox protocol provider. + */ +public class MboxProvider extends Provider { + public MboxProvider() { + super(Type.STORE, "mbox", MboxStore.class.getName(), + "Oracle", null); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxStore.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxStore.java new file mode 100644 index 0000000..13ead80 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/MboxStore.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import jakarta.mail.AuthenticationFailedException; +import jakarta.mail.Flags; +import jakarta.mail.Folder; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.URLName; + +public class MboxStore extends Store { + + String user; + String home; + Mailbox mb; + static Flags permFlags; + + static { + // we support all flags + permFlags = new Flags(); + permFlags.add(Flags.Flag.SEEN); + permFlags.add(Flags.Flag.RECENT); + permFlags.add(Flags.Flag.DELETED); + permFlags.add(Flags.Flag.FLAGGED); + permFlags.add(Flags.Flag.ANSWERED); + permFlags.add(Flags.Flag.DRAFT); + permFlags.add(Flags.Flag.USER); + } + + public MboxStore(Session session, URLName url) { + super(session, url); + + // XXX - handle security exception + user = System.getProperty("user.name"); + home = System.getProperty("user.home"); + String os = System.getProperty("os.name"); + try { + String cl = "org.xbib.net.mail.mbox." + os + "Mailbox"; + mb = (Mailbox) Class.forName(cl).getDeclaredConstructor().newInstance(); + } catch (Exception e) { + mb = new DefaultMailbox(); + } + } + + /** + * Since we do not have any authentication + * to do and we do not want a dialog put up asking the user for a + * password we always succeed in connecting. + * But if we're given a password, that means the user is + * doing something wrong so fail the request. + */ + protected boolean protocolConnect(String host, int port, String user, + String passwd) throws MessagingException { + + if (passwd != null) + throw new AuthenticationFailedException( + "mbox does not allow passwords"); + // XXX - should we use the user? + return true; + } + + protected void setURLName(URLName url) { + // host, user, password, and file don't matter so we strip them out + if (url != null && (url.getUsername() != null || + url.getHost() != null || + url.getFile() != null)) + url = new URLName(url.getProtocol(), null, -1, null, null, null); + super.setURLName(url); + } + + + public Folder getDefaultFolder() throws MessagingException { + checkConnected(); + + return new MboxFolder(this, null); + } + + public Folder getFolder(String name) throws MessagingException { + checkConnected(); + + return new MboxFolder(this, name); + } + + public Folder getFolder(URLName url) throws MessagingException { + checkConnected(); + return getFolder(url.getFile()); + } + + private void checkConnected() throws MessagingException { + if (!isConnected()) + throw new MessagingException("Not connected"); + } + + MailFile getMailFile(String folder) { + return mb.getMailFile(user, folder); + } + + Session getSession() { + return session; + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/MessageLoader.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/MessageLoader.java new file mode 100644 index 0000000..50d649d --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/MessageLoader.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import java.io.EOFException; +import java.io.FileDescriptor; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.List; + +/** + * A support class that contains the state and logic needed when + * loading messages from a folder. + */ +final class MessageLoader { + private final TempFile temp; + private FileInputStream fis = null; + private OutputStream fos = null; + private int pos, len; // position in and length of buffer + private long off; // current offset in temp file + private long prevend; // the end of the previous message in temp file + private MboxFolder.MessageMetadata md; + private byte[] buf = null; + // the length of the longest header we'll need to look at + private static final int LINELEN = "Content-Length: XXXXXXXXXX".length(); + private char[] line; + + public MessageLoader(TempFile temp) { + this.temp = temp; + } + + /** + * Load messages from the given file descriptor, starting at the + * specified offset, adding the MessageMetadata to the list.

+ * + * The data is assumed to be in UNIX mbox format, with newlines + * only as the line terminators. + */ + public int load(FileDescriptor fd, long offset, + List msgs) + throws IOException { + // XXX - could allocate and deallocate buffers here + int loaded = 0; + try { + fis = new FileInputStream(fd); + if (fis.skip(offset) != offset) + throw new EOFException("Failed to skip to offset " + offset); + this.off = prevend = temp.length(); + pos = len = 0; + line = new char[LINELEN]; + buf = new byte[64 * 1024]; + fos = temp.getAppendStream(); + int n; + // keep loading messages as long as we have headers + while ((n = skipHeader(loaded == 0)) >= 0) { + long start; + if (n == 0) { + // didn't find a Content-Length, skip the body + start = skipBody(); + if (start < 0) { + // md.end = -1; + md.dataend = -1; + msgs.add(md); + loaded++; + break; + } + md.dataend = start; + } else { + // skip over the body + skip(n); + md.dataend = off; + int b; + // skip any blank lines after the body + while ((b = get()) >= 0) { + if (b != '\n') + break; + } + start = off; + if (b >= 0) + start--; // back up one byte if not at EOF + } + // md.end = start; + prevend = start; + msgs.add(md); + loaded++; + } + } finally { + try { + fis.close(); + } catch (IOException ex) { + // ignore + } + try { + fos.close(); + } catch (IOException ex) { + // ignore + } + line = null; + buf = null; + } + return loaded; + } + + /** + * Skip over the message header, returning the content length + * of the body, or 0 if no Content-Length header was seen. + * Update the MessageMetadata based on the headers seen. + * return -1 on EOF. + */ + private int skipHeader(boolean first) throws IOException { + int clen = 0; + boolean bol = true; + int lpos = -1; + int b; + boolean saw_unix_from = false; + int lineno = 0; + md = new MboxFolder.MessageMetadata(); + md.start = prevend; + md.recent = true; + while ((b = get()) >= 0) { + if (bol) { + if (b == '\n') + break; + lpos = 0; + } + if (b == '\n') { + bol = true; + lineno++; + // newline at end of line, was the line one of the headers + // we're looking for? + if (lpos > 7) { + // XXX - make this more efficient? + String s = new String(line, 0, lpos); + // fast check for Content-Length header + if (lineno == 1 && line[0] == 'F' && isPrefix(s, "From ")) { + saw_unix_from = true; + } else if (line[7] == '-' && + isPrefix(s, "Content-Length:")) { + s = s.substring(15).trim(); + try { + clen = Integer.parseInt(s); + } catch (NumberFormatException ex) { + // ignore it + } + // fast check for Status header + } else if ((line[1] == 't' || line[1] == 'T') && + isPrefix(s, "Status:")) { + if (s.indexOf('O') >= 0) + md.recent = false; + // fast check for X-Status header + } else if ((line[3] == 't' || line[3] == 'T') && + isPrefix(s, "X-Status:")) { + if (s.indexOf('D') >= 0) + md.deleted = true; + // fast check for X-Dt-Delete-Time header + } else if (line[4] == '-' && + isPrefix(s, "X-Dt-Delete-Time:")) { + md.deleted = true; + // fast check for X-IMAP header + } else if (line[5] == 'P' && s.startsWith("X-IMAP:")) { + md.imap = true; + } + } + } else { + // accumlate data in line buffer + bol = false; + if (lpos < 0) // ignoring this line + continue; + if (lpos == 0 && (b == ' ' || b == '\t')) + lpos = -1; // ignore continuation lines + else if (lpos < line.length) + line[lpos++] = (char) b; + } + } + + // if we hit EOF, or this is the first message we're loading and + // it doesn't have a UNIX From line, return EOF. + // (After the first message, UNIX From lines are seen by skipBody + // to terminate the message.) + if (b < 0 || (first && !saw_unix_from)) + return -1; + else + return clen; + } + + /** + * Does "s" start with "pre", ignoring case? + */ + private static final boolean isPrefix(String s, String pre) { + return s.regionMatches(true, 0, pre, 0, pre.length()); + } + + /** + * Skip over the body of the message looking for a line that starts + * with "From ". If found, return the offset of the beginning of + * that line. Return -1 on EOF. + */ + private long skipBody() throws IOException { + boolean bol = true; + int lpos = -1; + long loff = off; + int b; + while ((b = get()) >= 0) { + if (bol) { + lpos = 0; + loff = off - 1; + } + if (b == '\n') { + bol = true; + if (lpos >= 5) { // have enough data to test? + if (line[0] == 'F' && line[1] == 'r' && line[2] == 'o' && + line[3] == 'm' && line[4] == ' ') + return loff; + } + } else { + bol = false; + if (lpos < 0) + continue; + if (lpos == 0 && b != 'F') + lpos = -1; // ignore lines that don't start with F + else if (lpos < 5) // only need first 5 chars to test + line[lpos++] = (char) b; + } + } + return -1; + } + + /** + * Skip "n" bytes, returning how much we were able to skip. + */ + private final int skip(int n) throws IOException { + int n0 = n; + if (pos + n < len) { + pos += n; // can do it all within this buffer + off += n; + } else { + do { + n -= (len - pos); // skip rest of this buffer + off += (len - pos); + fill(); + if (len <= 0) // ran out of data + return n0 - n; + } while (n > len); + pos += n; + off += n; + } + return n0; + } + + /** + * Return the next byte. + */ + private final int get() throws IOException { + if (pos >= len) + fill(); + if (pos >= len) + return -1; + else { + off++; + return buf[pos++] & 0xff; + } + } + + /** + * Fill our buffer with more data. + * Every buffer we read is also written to the temp file. + */ + private final void fill() throws IOException { + len = fis.read(buf); + pos = 0; + if (len > 0) + fos.write(buf, 0, len); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/NewlineOutputStream.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/NewlineOutputStream.java new file mode 100644 index 0000000..f7711f3 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/NewlineOutputStream.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; + +/** + * Convert the various newline conventions to the local platform's + * newline convention. Optionally, make sure the output ends with + * a blank line. + */ +public class NewlineOutputStream extends FilterOutputStream { + private int lastb = -1; + private int bol = 1; // number of times in a row we're at beginning of line + private final boolean endWithBlankLine; + private static final byte[] newline; + + static { + String s = System.lineSeparator(); + if (s == null || s.length() <= 0) + s = "\n"; + newline = s.getBytes(StandardCharsets.ISO_8859_1); + } + + public NewlineOutputStream(OutputStream os) { + this(os, false); + } + + public NewlineOutputStream(OutputStream os, boolean endWithBlankLine) { + super(os); + this.endWithBlankLine = endWithBlankLine; + } + + public void write(int b) throws IOException { + if (b == '\r') { + out.write(newline); + bol++; + } else if (b == '\n') { + if (lastb != '\r') { + out.write(newline); + bol++; + } + } else { + out.write(b); + bol = 0; // no longer at beginning of line + } + lastb = b; + } + + public void write(byte[] b) throws IOException { + write(b, 0, b.length); + } + + public void write(byte[] b, int off, int len) throws IOException { + for (int i = 0; i < len; i++) { + write(b[off + i]); + } + } + + public void flush() throws IOException { + if (endWithBlankLine) { + if (bol == 0) { + // not at bol, return to bol and add a blank line + out.write(newline); + out.write(newline); + } else if (bol == 1) { + // at bol, add a blank line + out.write(newline); + } + } + bol = 2; + out.flush(); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/SolarisMailbox.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/SolarisMailbox.java new file mode 100644 index 0000000..7954ed9 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/SolarisMailbox.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import java.io.File; + +public class SolarisMailbox extends Mailbox { + private final String home; + private final String user; + + private static final boolean homeRelative = + Boolean.getBoolean("mail.mbox.homerelative"); + + /** + * Creates a default {@code SolarisMailbox}. + */ + public SolarisMailbox() { + String h = System.getenv("HOME"); + if (h == null) + h = System.getProperty("user.home"); + home = h; + user = System.getProperty("user.name"); + } + + public MailFile getMailFile(String user, String folder) { + if (folder.equalsIgnoreCase("INBOX")) + return new UNIXInbox(user, filename(user, folder)); + else + return new UNIXFolder(filename(user, folder)); + } + + /** + * Given a name of a mailbox folder, expand it to a full path name. + */ + public String filename(String user, String folder) { + try { + switch (folder.charAt(0)) { + case '/': + return folder; + case '~': + int i = folder.indexOf(File.separatorChar); + String tail = ""; + if (i > 0) { + tail = folder.substring(i); + folder = folder.substring(0, i); + } + if (folder.length() == 1) + return home + tail; + else + return "/home/" + folder.substring(1) + tail; // XXX + default: + if (folder.equalsIgnoreCase("INBOX")) { + if (user == null) // XXX - should never happen + user = this.user; + String inbox = System.getenv("MAIL"); + if (inbox == null) + inbox = "/var/mail/" + user; + return inbox; + } else { + if (homeRelative) + return home + File.separator + folder; + else + return folder; + } + } + } catch (StringIndexOutOfBoundsException e) { + return folder; + } + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/SunOSMailbox.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/SunOSMailbox.java new file mode 100644 index 0000000..21f4278 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/SunOSMailbox.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +public class SunOSMailbox extends SolarisMailbox { + + /** + * Creates a default {@code SunOSMailbox}. + * + */ + public SunOSMailbox() { + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/SunV3BodyPart.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/SunV3BodyPart.java new file mode 100644 index 0000000..093fbde --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/SunV3BodyPart.java @@ -0,0 +1,317 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import jakarta.activation.DataHandler; +import jakarta.mail.IllegalWriteException; +import jakarta.mail.MessagingException; +import jakarta.mail.MethodNotSupportedException; +import jakarta.mail.internet.InternetHeaders; +import jakarta.mail.internet.MimeBodyPart; +import java.io.IOException; +import java.io.OutputStream; + +/** + * This class represents a SunV3 BodyPart. + * + * @author Bill Shannon + * @see jakarta.mail.Part + * @see jakarta.mail.internet.MimePart + * @see MimeBodyPart + */ + +public class SunV3BodyPart extends MimeBodyPart { + /** + * Constructs a SunV3BodyPart using the given header and + * content bytes.

+ * + * Used by providers. + * + * @param headers The header of this part + * @param content bytes representing the body of this part. + */ + public SunV3BodyPart(InternetHeaders headers, byte[] content) + throws MessagingException { + super(headers, content); + } + + /** + * Return the size of the content of this BodyPart in bytes. + * Return -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 in bytes + */ + public int getSize() throws MessagingException { + String s = getHeader("X-Sun-Content-Length", null); + try { + return Integer.parseInt(s); + } catch (NumberFormatException ex) { + return -1; + } + } + + /** + * Return the number of lines for the content of this Part. + * Return -1 if this number cannot be determined.

+ * + * Note that this number may not be an exact measure of the + * content length and may or may not account for any transfer + * encoding of the content. + */ + public int getLineCount() throws MessagingException { + String s = getHeader("X-Sun-Content-Lines", null); + try { + return Integer.parseInt(s); + } catch (NumberFormatException ex) { + return -1; + } + } + + /* + * This is just enough to get us going. + * + * For more complete transformation from V3 to MIME, refer to + * sun_att.c from the Sun IMAP server code. + */ + static class MimeV3Map { + String mime; + String v3; + + MimeV3Map(String mime, String v3) { + this.mime = mime; + this.v3 = v3; + } + + private static MimeV3Map[] mimeV3Table = new MimeV3Map[]{ + new MimeV3Map("text/plain", "text"), + new MimeV3Map("text/plain", "default"), + new MimeV3Map("multipart/x-sun-attachment", "X-sun-attachment"), + new MimeV3Map("application/postscript", "postscript-file"), + new MimeV3Map("image/gif", "gif-file") + // audio-file + // cshell-script + }; + + // V3 Content-Type to MIME Content-Type + static String toMime(String s) { + for (int i = 0; i < mimeV3Table.length; i++) { + if (mimeV3Table[i].v3.equalsIgnoreCase(s)) + return mimeV3Table[i].mime; + } + return "application/x-" + s; + } + + // MIME Content-Type to V3 Content-Type + static String toV3(String s) { + for (int i = 0; i < mimeV3Table.length; i++) { + if (mimeV3Table[i].mime.equalsIgnoreCase(s)) + return mimeV3Table[i].v3; + } + return s; + } + } + + /** + * Returns the value of the RFC822 "Content-Type" header field. + * This represents the content-type of the content of this + * BodyPart. This value must not be null. If this field is + * unavailable, "text/plain" should be returned.

+ * + * This implementation uses getHeader(name) + * to obtain the requisite header field. + * + * @return Content-Type of this BodyPart + */ + public String getContentType() throws MessagingException { + String ct = getHeader("Content-Type", null); + if (ct == null) + ct = getHeader("X-Sun-Data-Type", null); + if (ct == null) + ct = "text/plain"; + else if (ct.indexOf('/') < 0) + ct = MimeV3Map.toMime(ct); + return ct; + } + + /** + * Returns the value of the "Content-Transfer-Encoding" header + * field. Returns null if the header is unavailable + * or its value is absent.

+ * + * This implementation uses getHeader(name) + * to obtain the requisite header field. + * + * @see #headers + */ + public String getEncoding() throws MessagingException { + String enc = super.getEncoding(); + if (enc == null) + enc = getHeader("X-Sun-Encoding-Info", null); + return enc; + } + + /** + * Returns the "Content-Description" header field of this BodyPart. + * This typically associates some descriptive information with + * this part. Returns null if this field is unavailable or its + * value is absent.

+ * + * If the Content-Description field is encoded as per RFC 2047, + * it is decoded and converted into Unicode. If the decoding or + * conversion fails, the raw data is returned as-is

+ * + * This implementation uses getHeader(name) + * to obtain the requisite header field. + * + * @return content-description + */ + public String getDescription() throws MessagingException { + String desc = super.getDescription(); + if (desc == null) + desc = getHeader("X-Sun-Data-Description", null); + return desc; + } + + /** + * Set the "Content-Description" header field for this BodyPart. + * If the description parameter is null, then any + * existing "Content-Description" fields are removed.

+ * + * If the description contains non US-ASCII characters, it will + * be encoded using the platform's default charset. If the + * description contains only US-ASCII characters, no encoding + * is done and it is used as-is. + * + * @param description content-description + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this BodyPart is + * obtained from a READ_ONLY folder. + */ + public void setDescription(String description) throws MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart not writable"); + } + + /** + * Set the "Content-Description" header field for this BodyPart. + * If the description parameter is null, then any + * existing "Content-Description" fields are removed.

+ * + * If the description contains non US-ASCII characters, it will + * be encoded using the specified charset. If the description + * contains only US-ASCII characters, no encoding is done and + * it is used as-is + * + * @param description Description + * @param charset Charset for encoding + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this BodyPart is + * obtained from a READ_ONLY folder. + */ + public void setDescription(String description, String charset) + throws MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart not writable"); + } + + /** + * Get the filename associated with this BodyPart.

+ * + * Returns the value of the "filename" parameter from the + * "Content-Disposition" header field of this BodyPart. If its + * not available, returns the value of the "name" parameter from + * the "Content-Type" header field of this BodyPart. + * Returns null if both are absent. + * + * @return filename + */ + public String getFileName() throws MessagingException { + String name = super.getFileName(); + if (name == null) + name = getHeader("X-Sun-Data-Name", null); + return name; + } + + /** + * Set the filename associated with this BodyPart, if possible.

+ * + * Sets the "filename" parameter of the "Content-Disposition" + * header field of this BodyPart. + * + * @exception IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this BodyPart is + * obtained from a READ_ONLY folder. + */ + public void setFileName(String filename) throws MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart not writable"); + } + + /** + * This method provides the mechanism to set this BodyPart's content. + * The given DataHandler object should wrap the actual content. + * + * @param dh The DataHandler for the content + * @throws IllegalWriteException if the underlying + * implementation does not support modification + * @exception IllegalStateException if this BodyPart is + * obtained from a READ_ONLY folder. + */ + public void setDataHandler(DataHandler dh) + throws MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart not writable"); + } + + /** + * Output the BodyPart as a RFC822 format stream. + * + * @throws IOException if an error occurs writing to the + * stream or if an error is generated + * by the jakarta.activation layer. + * @see DataHandler#writeTo(OutputStream) + */ + public void writeTo(OutputStream os) + throws IOException, MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart writeTo"); + } + + /** + * This is the method that has the 'smarts' to query the 'content' + * and update the appropriate headers. Typical headers that get + * set here are: Content-Type, Content-Encoding, boundary (for + * multipart). Now, the tricky part here is when to actually + * activate this method: + * + * - A Message being crafted by a mail-application will certainly + * need to activate this method at some point to fill up its internal + * headers. Typically this is triggered off by our writeTo() method. + * + * - A message read-in from a MessageStore will have obtained + * all its headers from the store, and so does'nt need this. + * However, if this message is editable and if any edits have + * been made to either the content or message-structure, we might + * need to resync our headers. Typically this is triggered off by + * the Message.saveChanges() methods. + */ + protected void updateHeaders() throws MessagingException { + throw new MethodNotSupportedException("SunV3BodyPart updateHeaders"); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/SunV3Multipart.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/SunV3Multipart.java new file mode 100644 index 0000000..ab6b84a --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/SunV3Multipart.java @@ -0,0 +1,206 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import jakarta.activation.DataSource; +import jakarta.mail.BodyPart; +import jakarta.mail.MessagingException; +import jakarta.mail.MethodNotSupportedException; +import jakarta.mail.internet.InternetHeaders; +import jakarta.mail.internet.MimeMultipart; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import org.xbib.net.mail.util.LineInputStream; + +/** + * The SunV3Multipart class is an implementation of the abstract Multipart + * class that uses SunV3 conventions for the multipart data.

+ * + * @author Bill Shannon + */ + +public class SunV3Multipart extends MimeMultipart { + private boolean parsing; + + /** + * Constructs a SunV3Multipart object and its bodyparts from the + * given DataSource.

+ * + * @param ds DataSource, can be a MultipartDataSource + */ + public SunV3Multipart(DataSource ds) throws MessagingException { + super(ds); + } + + /** + * Set the subtype. Throws MethodNotSupportedException. + * + * @param subtype Subtype + */ + public void setSubType(String subtype) throws MessagingException { + throw new MethodNotSupportedException( + "can't change SunV3Multipart subtype"); + } + + /** + * Get the BodyPart referred to by the given ContentID (CID). + * Throws MethodNotSupportException. + */ + public synchronized BodyPart getBodyPart(String CID) + throws MessagingException { + throw new MethodNotSupportedException( + "SunV3Multipart doesn't support Content-ID"); + } + + /** + * Update headers. Throws MethodNotSupportException. + */ + protected void updateHeaders() throws MessagingException { + throw new MethodNotSupportedException("SunV3Multipart not writable"); + } + + /** + * Iterates through all the parts and outputs each SunV3 part + * separated by a boundary. + */ + public void writeTo(OutputStream os) + throws IOException, MessagingException { + throw new MethodNotSupportedException( + "SunV3Multipart writeTo not supported"); + } + + private static final String boundary = "----------"; + + /* + * Parse the contents of this multipart message and create the + * child body parts. + */ + protected synchronized void parse() throws MessagingException { + /* + * If the data has already been parsed, or we're in the middle of + * parsing it, there's nothing to do. The latter will occur when + * we call addBodyPart, which will call parse again. We really + * want to be able to call super.super.addBodyPart. + */ + if (parsed || parsing) + return; + + InputStream in = null; + + try { + in = ds.getInputStream(); + if (!(in instanceof ByteArrayInputStream) && + !(in instanceof BufferedInputStream)) + in = new BufferedInputStream(in); + } catch (IOException | RuntimeException ex) { + throw new MessagingException("No inputstream from datasource"); + } + + byte[] bndbytes = boundary.getBytes(StandardCharsets.ISO_8859_1); + int bl = bndbytes.length; + + String line; + parsing = true; + try { + /* + * Skip any kind of junk until we get to the first + * boundary line. + */ + LineInputStream lin = new LineInputStream(in); + while ((line = lin.readLine()) != null) { + if (line.trim().equals(boundary)) + break; + } + if (line == null) + throw new MessagingException("Missing start boundary"); + + /* + * Read and process body parts until we see the + * terminating boundary line (or EOF). + */ + for (; ; ) { + /* + * Collect the headers for this body part. + */ + InternetHeaders headers = new InternetHeaders(in); + + if (!in.markSupported()) + throw new MessagingException("Stream doesn't support mark"); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + int b; + + /* + * Read and save the content bytes in buf. + */ + while ((b = in.read()) >= 0) { + if (b == '\r' || b == '\n') { + /* + * Found the end of a line, check whether the + * next line is a boundary. + */ + int i; + in.mark(bl + 4 + 1); // "4" for possible "--\r\n" + if (b == '\r' && in.read() != '\n') { + in.reset(); + in.mark(bl + 4); + } + // read bytes, matching against the boundary + for (i = 0; i < bl; i++) + if (in.read() != bndbytes[i]) + break; + if (i == bl) { + int b2 = in.read(); + // check for end of line + if (b2 == '\n') + break; // got it! break out of the while loop + if (b2 == '\r') { + in.mark(1); + if (in.read() != '\n') + in.reset(); + break; // got it! break out of the while loop + } + } + // failed to match, reset and proceed normally + in.reset(); + } + buf.write(b); + } + + /* + * Create a SunV3BodyPart to represent this body part. + */ + SunV3BodyPart body = + new SunV3BodyPart(headers, buf.toByteArray()); + addBodyPart(body); + if (b < 0) + break; + } + } catch (IOException e) { + throw new MessagingException("IO Error"); // XXX + } finally { + parsing = false; + } + + parsed = true; + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/TempFile.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/TempFile.java new file mode 100644 index 0000000..d6eb470 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/TempFile.java @@ -0,0 +1,174 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.mbox; + +import jakarta.mail.util.SharedFileInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.RandomAccessFile; + +/** + * A temporary file used to cache 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("mbox.", ".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(); + } + + /** + * Return a stream for reading from part of the file. + */ + public InputStream newStream(long start, long end) { + return sf.newStream(start, end); + } + + public long length() { + return file.length(); + } + + /** + * Close and remove this temp file. + */ + public void close() { + try { + sf.close(); + } catch (IOException ex) { + // ignore it + } + file.delete(); + } +} + +/** + * 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. + */ + 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( + "file cache only supports single threaded access"); + af = new AppendStream(this); + return af; + } +} + +/** + * 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); + } + + public void write(int b) throws IOException { + raf.write(b); + } + + public void write(byte[] b) throws IOException { + raf.write(b); + } + + public void write(byte[] b, int off, int len) throws IOException { + raf.write(b, off, len); + } + + 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/net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXFile.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXFile.java new file mode 100644 index 0000000..ce26b27 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXFile.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import java.io.File; +import java.io.FileDescriptor; +import java.util.StringTokenizer; + +@SuppressWarnings("serial") +public class UNIXFile extends File { + + protected static final boolean loaded; + protected static final int lockType; + + public UNIXFile(String name) { + super(name); + } + + // lock type enum + protected static final int NONE = 0; + protected static final int NATIVE = 1; + protected static final int JAVA = 2; + + static { + String lt = System.getProperty("mail.mbox.locktype", "native"); + int type = NATIVE; + if (lt.equalsIgnoreCase("none")) + type = NONE; + else if (lt.equalsIgnoreCase("java")) + type = JAVA; + lockType = type; + + boolean lloaded = false; + if (lockType == NATIVE) { + try { + System.loadLibrary("mbox"); + lloaded = true; + } catch (UnsatisfiedLinkError e) { + String classpath = System.getProperty("java.class.path"); + String sep = System.getProperty("path.separator"); + String arch = System.getProperty("os.arch"); + StringTokenizer st = new StringTokenizer(classpath, sep); + while (st.hasMoreTokens()) { + String path = st.nextToken(); + if (path.endsWith("/classes") || + path.endsWith("/mail.jar") || + path.endsWith("/jakarta.mail.jar")) { + int i = path.lastIndexOf('/'); + String libdir = path.substring(0, i + 1) + "lib/"; + String lib = libdir + arch + "/libmbox.so"; + try { + System.load(lib); + lloaded = true; + break; + } catch (UnsatisfiedLinkError e2) { + lib = libdir + "libmbox.so"; + try { + System.load(lib); + lloaded = true; + break; + } catch (UnsatisfiedLinkError e3) { + continue; + } + } + } + } + } + } + loaded = lloaded; + if (loaded) + initIDs(FileDescriptor.class, FileDescriptor.in); + } + + /** + * Return the access time of the file. + */ + public static long lastAccessed(File file) { + return lastAccessed0(file.getPath()); + } + + public long lastAccessed() { + return lastAccessed0(getPath()); + } + + private static native void initIDs(Class fdClass, + FileDescriptor stdin); + + /** + * Lock the file referred to by fd. The string mode is "r" + * for a read lock or "rw" for a write lock. Don't block + * if lock can't be acquired. + */ + public static boolean lock(FileDescriptor fd, String mode) { + return lock(fd, mode, false); + } + + /** + * Lock the file referred to by fd. The string mode is "r" + * for a read lock or "rw" for a write lock. If block is set, + * block waiting for the lock if necessary. + */ + private static boolean lock(FileDescriptor fd, String mode, boolean block) { + //return loaded && lock0(fd, mode); + if (loaded) { + boolean ret; + ret = lock0(fd, mode, block); + return ret; + } + return false; + } + + private static native boolean lock0(FileDescriptor fd, String mode, boolean block); + + public static native long lastAccessed0(String name); +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXFolder.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXFolder.java new file mode 100644 index 0000000..0bec55f --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXFolder.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import java.io.FileDescriptor; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.RandomAccessFile; + +@SuppressWarnings("serial") +public class UNIXFolder extends UNIXFile implements MailFile { + protected transient RandomAccessFile file; + + public UNIXFolder(String name) { + super(name); + } + + public boolean lock(String mode) { + try { + file = new RandomAccessFile(this, mode); + switch (lockType) { + case NONE: + return true; + case NATIVE: + default: + return lock(file.getFD(), mode); + case JAVA: + return file.getChannel(). + tryLock(0L, Long.MAX_VALUE, !mode.equals("rw")) != null; + } + } catch (FileNotFoundException fe) { + return false; + } catch (IOException ie) { + file = null; + return false; + } + } + + public void unlock() { + if (file != null) { + try { + file.close(); + } catch (IOException e) { + // ignore it + } + file = null; + } + } + + public void touchlock() { + } + + public FileDescriptor getFD() { + if (file == null) + return null; + try { + return file.getFD(); + } catch (IOException e) { + return null; + } + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXInbox.java b/net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXInbox.java new file mode 100644 index 0000000..c6c96d1 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/mbox/UNIXInbox.java @@ -0,0 +1,126 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.mbox; + +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + +@SuppressWarnings("serial") +public class UNIXInbox extends UNIXFolder implements InboxFile { + private final String user; + + /* + * Superclass UNIXFile loads the library containing all the + * native code and sets the "loaded" flag if successful. + */ + + public UNIXInbox(String user, String name) { + super(name); + this.user = user; + if (user == null) + throw new NullPointerException("user name is null in UNIXInbox"); + } + + public boolean lock(String mode) { + if (lockType == NATIVE) { + if (!loaded) + return false; + if (!maillock(user, 5)) + return false; + } + if (!super.lock(mode)) { + if (loaded) + mailunlock(); + return false; + } + return true; + } + + public void unlock() { + super.unlock(); + if (loaded) + mailunlock(); + } + + public void touchlock() { + if (loaded) + touchlock0(); + } + + private transient RandomAccessFile lockfile; // the user's ~/.Maillock file + private transient String lockfileName; // its name + + public boolean openLock(String mode) { + if (mode.equals("r")) + return true; + if (lockfileName == null) { + String home = System.getProperty("user.home"); + lockfileName = home + File.separator + ".Maillock"; + } + try { + lockfile = new RandomAccessFile(lockfileName, mode); + boolean ret; + switch (lockType) { + case NONE: + ret = true; + break; + case NATIVE: + default: + ret = UNIXFile.lock(lockfile.getFD(), mode); + break; + case JAVA: + ret = lockfile.getChannel(). + tryLock(0L, Long.MAX_VALUE, !mode.equals("rw")) != null; + break; + } + if (!ret) + closeLock(); + return ret; + } catch (IOException ex) { + } + return false; + } + + public void closeLock() { + if (lockfile == null) + return; + try { + lockfile.close(); + } catch (IOException ex) { + } finally { + lockfile = null; + } + } + + public boolean equals(Object o) { + if (!(o instanceof UNIXInbox)) + return false; + UNIXInbox other = (UNIXInbox) o; + return user.equals(other.user) && super.equals(other); + } + + public int hashCode() { + return super.hashCode() + user.hashCode(); + } + + private native boolean maillock(String user, int retryCount); + + private native void mailunlock(); + + private native void touchlock0(); +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/package-info.java b/net-mail/src/main/java/org/xbib/net/mail/package-info.java new file mode 100644 index 0000000..d791e64 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/package-info.java @@ -0,0 +1,339 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * The Jakarta Mail API + * provides classes that model a mail system. + * The jakarta.mail package defines classes that are common to + * all mail systems. + * The jakarta.mail.internet package defines classes that are specific + * to mail systems based on internet standards such as MIME, SMTP, POP3, and IMAP. + * The Jakarta Mail API includes the jakarta.mail package and subpackages. + * + *

+ * For an overview of the Jakarta Mail API, read the + * + * Jakarta Mail specification. + *

+ *

+ * The code to send a plain text message can be as simple as the following: + *

+ *
+ * Properties props = new Properties();
+ * props.put("mail.smtp.host", "my-mail-server");
+ * Session session = Session.getInstance(props, null);
+ * 

+ * try { + * MimeMessage msg = new MimeMessage(session); + * msg.setFrom("me@example.com"); + * msg.setRecipients(Message.RecipientType.TO, + * "you@example.com"); + * msg.setSubject("Jakarta Mail hello world example"); + * msg.setSentDate(new Date()); + * msg.setText("Hello, world!\n"); + * Transport.send(msg, "me@example.com", "my-password"); + * } catch (MessagingException mex) { + * System.out.println("send failed, exception: " + mex); + * } + *

+ *

+ * The Jakarta Mail download bundle contains many more complete examples + * in the "demo" directory. + *

+ *

+ * Don't forget to see the + * + * Jakarta Mail API FAQ + * for answers to the most common questions. + * The + * Jakarta Mail web site + * contains many additional resources. + *

+ * Properties + *

+ * The Jakarta Mail API supports the following standard properties, + * which may be set in the Session object, or in the + * Properties object used to create the Session object. + * The properties are always set as strings; the Type column describes + * how the string is interpreted. For example, use + *

+ *
+ * props.put("mail.debug", "true");
+ * 
+ *

+ * to set the mail.debug property, which is of type boolean. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Jakarta Mail properties
NameTypeDescription
mail.debugboolean + * The initial debug mode. + * Default is false. + *
mail.fromString + * The return email address of the current user, used by the + * InternetAddress method getLocalAddress. + *
mail.mime.address.strictboolean + * The MimeMessage class uses the InternetAddress method + * parseHeader to parse headers in messages. This property + * controls the strict flag passed to the parseHeader + * method. The default is true. + *
mail.hostString + * The default host name of the mail server for both Stores and Transports. + * Used if the mail.protocol.host property isn't set. + *
mail.store.protocolString + * Specifies the default message access protocol. The + * Session method getStore() returns a Store + * object that implements this protocol. By default the first Store + * provider in the configuration files is returned. + *
mail.transport.protocolString + * Specifies the default message transport protocol. The + * Session method getTransport() returns a Transport + * object that implements this protocol. By default the first Transport + * provider in the configuration files is returned. + *
mail.userString + * The default user name to use when connecting to the mail server. + * Used if the mail.protocol.user property isn't set. + *
mail.protocol.classString + * Specifies the fully qualified class name of the provider for the + * specified protocol. Used in cases where more than one provider + * for a given protocol exists; this property can be used to specify + * which provider to use by default. The provider must still be listed + * in a configuration file. + *
mail.protocol.hostString + * The host name of the mail server for the specified protocol. + * Overrides the mail.host property. + *
mail.protocol.portint + * The port number of the mail server for the specified protocol. + * If not specified the protocol's default port number is used. + *
mail.protocol.userString + * The user name to use when connecting to mail servers + * using the specified protocol. + * Overrides the mail.user property. + *
+ * + *

+ * The following properties are supported by the EE4J implementation of + * Jakarta Mail, but are not currently a required part of the specification. + * The names, types, defaults, and semantics of these properties may + * change in future releases. + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Jakarta Mail implementation properties
NameTypeDescription
mail.debug.authboolean + * Include protocol authentication commands (including usernames and passwords) + * in the debug output. + * Default is false. + *
mail.debug.auth.usernameboolean + * Include the user name in non-protocol debug output. + * Default is true. + *
mail.debug.auth.passwordboolean + * Include the password in non-protocol debug output. + * Default is false. + *
mail.transport.protocol.address-typeString + * Specifies the default message transport protocol for the specified address type. + * The Session method getTransport(Address) returns a + * Transport object that implements this protocol when the address is of the + * specified type (e.g., "rfc822" for standard internet addresses). + * By default the first Transport configured for that address type is used. + * This property can be used to override the behavior of the + * {@link jakarta.mail.Transport#send send} method of the + * {@link jakarta.mail.Transport Transport} class so that (for example) the "smtps" + * protocol is used instead of the "smtp" protocol by setting the property + * mail.transport.protocol.rfc822 to "smtps". + *
mail.event.scopeString + * Controls the scope of events. (See the jakarta.mail.event package.) + * By default, a separate event queue and thread is used for events for each + * Store, Transport, or Folder. + * If this property is set to "session", all such events are put in a single + * event queue processed by a single thread for the current session. + * If this property is set to "application", all such events are put in a single + * event queue processed by a single thread for the current application. + * (Applications are distinguished by their context class loader.) + *
mail.event.executorjava.util.concurrent.Executor + * By default, a new Thread is created for each event queue. + * This thread is used to call the listeners for these events. + * If this property is set to an instance of an Executor, the + * Executor.execute method is used to run the event dispatcher + * for an event queue. The event dispatcher runs until the + * event queue is no longer in use. + *
+ * + *

+ * The Jakarta Mail API also supports several System properties; + * see the {@link jakarta.mail.internet} package documentation + * for details. + *

+ *

+ * The Jakarta Mail reference + * implementation includes protocol providers in subpackages of + * org.xbib.net.mail. Note that the APIs to these protocol + * providers are not part of the standard Jakarta Mail API. Portable + * programs will not use these APIs. + *

+ *

+ * Nonportable programs may use the APIs of the protocol providers + * by (for example) casting a returned Folder object to a + * org.xbib.net.mail.imap.IMAPFolder object. Similarly for + * Store and Message objects returned from the + * standard Jakarta Mail APIs. + *

+ *

+ * The protocol providers also support properties that are specific to + * those providers. The package documentation for the + * {@code org.xbib.net.mail.imap IMAP}, {@code org.xbib.net.mail.pop3 POP3}, + * and {@code org.xbib.net.mail.smtp SMTP} packages provide details. + *

+ *

+ * In addition to printing debugging output as controlled by the + * {@link jakarta.mail.Session Session} configuration, the current + * implementation of classes in this package log the same information using + * {@link java.util.logging.Logger} as described in the following table: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Jakarta Mail Loggers
Logger NameLogging LevelPurpose
jakarta.mailCONFIGConfiguration of the Session
jakarta.mailFINEGeneral debugging output
+ */ +package org.xbib.net.mail; \ No newline at end of file diff --git a/net-mail/src/main/java/org/xbib/net/mail/pop3/AppendStream.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/AppendStream.java new file mode 100644 index 0000000..67ebb91 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/AppendStream.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.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/net-mail/src/main/java/org/xbib/net/mail/pop3/DefaultFolder.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/DefaultFolder.java new file mode 100644 index 0000000..aed9da4 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/DefaultFolder.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.pop3; + +import jakarta.mail.Flags; +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.MethodNotSupportedException; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Folder.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Folder.java new file mode 100644 index 0000000..88bbfb0 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Folder.java @@ -0,0 +1,603 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.pop3; + +import jakarta.mail.AuthenticationFailedException; +import jakarta.mail.FetchProfile; +import jakarta.mail.Flags; +import jakarta.mail.Folder; +import jakarta.mail.FolderClosedException; +import jakarta.mail.FolderNotFoundException; +import jakarta.mail.Message; +import jakarta.mail.MessageRemovedException; +import jakarta.mail.MessagingException; +import jakarta.mail.MethodNotSupportedException; +import jakarta.mail.UIDFolder; +import jakarta.mail.event.ConnectionEvent; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Constructor; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.mail.util.LineInputStream; + +/** + * A POP3 Folder (can only be "INBOX"). + * + * See the org.xbib.net.mail.pop3 package + * documentation for further information on the POP3 protocol provider.

+ * + * @author Bill Shannon + * @author John Mani (ported to the jakarta.mail APIs) + */ +public class POP3Folder extends Folder { + + private static final Logger logger = Logger.getLogger(POP3Folder.class.getName()); + + 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; + + protected POP3Folder(POP3Store store, String name) { + super(store); + this.name = name; + this.store = store; + if (name.equalsIgnoreCase("INBOX")) + exists = true; + } + + @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. + * + * @return unique ID string + * @param msg the message + * @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(); + } + + /* 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/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Message.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Message.java new file mode 100644 index 0000000..cea2d9f --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Message.java @@ -0,0 +1,659 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.pop3; + +import jakarta.mail.Flags; +import jakarta.mail.Folder; +import jakarta.mail.FolderClosedException; +import jakarta.mail.Header; +import jakarta.mail.IllegalWriteException; +import jakarta.mail.MessageRemovedException; +import jakarta.mail.MessagingException; +import jakarta.mail.event.MessageChangedEvent; +import jakarta.mail.internet.InternetHeaders; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.SharedInputStream; +import java.io.BufferedOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.ref.SoftReference; +import java.util.Enumeration; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.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 { + + private static final Logger logger = Logger.getLogger(POP3Message.class.getName()); + + /* + * 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 (logger.isLoggable(Level.FINE)) + 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. + */ + 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 jakarta.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 + * @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 + * @see jakarta.mail.internet.MimeUtility + */ + @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 + * @exception IllegalWriteException because the underlying + * implementation does not support modification + * @exception IllegalStateException if this message is + * obtained from a READ_ONLY folder. + * @see jakarta.mail.internet.MimeUtility + */ + @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 jakarta.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. + * + * @throws IOException if an error occurs writing to the stream + * or if an error is generated by the + * jakarta.activation layer. + * @throws MessagingException for other failures + * @see jakarta.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 (logger.isLoggable(Level.FINE)) + 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/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Provider.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Provider.java new file mode 100644 index 0000000..f3b1a8d --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Provider.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.pop3; + +import jakarta.mail.Provider; +import org.xbib.net.mail.util.DefaultProvider; + +/** + * The POP3 protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class POP3Provider extends Provider { + public POP3Provider() { + super(Type.STORE, "pop3", POP3Store.class.getName(), + "Oracle", null); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3SSLProvider.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3SSLProvider.java new file mode 100644 index 0000000..a1d10f7 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3SSLProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.pop3; + +import jakarta.mail.Provider; +import org.xbib.net.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(Type.STORE, "pop3s", POP3SSLStore.class.getName(), + "Oracle", null); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3SSLStore.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3SSLStore.java new file mode 100644 index 0000000..e2172fe --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3SSLStore.java @@ -0,0 +1,32 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.pop3; + +import jakarta.mail.Session; +import jakarta.mail.URLName; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Store.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Store.java new file mode 100644 index 0000000..1c63d1a --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/POP3Store.java @@ -0,0 +1,534 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.pop3; + +import jakarta.mail.AuthenticationFailedException; +import jakarta.mail.Folder; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.URLName; +import java.io.EOFException; +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Constructor; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.mail.util.MailConnectException; +import org.xbib.net.mail.util.PropUtil; +import org.xbib.net.mail.util.SocketConnectException; + +/** + * A POP3 Message Store. Contains only one folder, "INBOX". + * + * See the org.xbib.net.mail.pop3 package + * documentation for further information on the POP3 protocol provider.

+ * + * @author Bill Shannon + * @author John Mani + */ +public class POP3Store extends Store { + + private static final Logger logger = Logger.getLogger(POP3Store.class.getName()); + + 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; + + // 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; + 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 = {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 + public 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, 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; + } + + private void checkConnected() throws MessagingException { + if (!super.isConnected()) + throw new MessagingException("Not connected"); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/pop3/Protocol.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/Protocol.java new file mode 100644 index 0000000..4a73907 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/Protocol.java @@ -0,0 +1,1246 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.pop3; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.InterruptedIOException; +import java.io.OutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.Socket; +import java.net.SocketException; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SSLSocket; +import org.xbib.net.security.auth.Ntlm; +import org.xbib.net.mail.util.ASCIIUtility; +import org.xbib.net.mail.util.BASE64EncoderStream; +import org.xbib.net.mail.util.LineInputStream; +import org.xbib.net.mail.util.PropUtil; +import org.xbib.net.mail.util.SharedByteArrayOutputStream; +import org.xbib.net.mail.util.SocketFetcher; +import org.xbib.net.mail.util.TraceInputStream; +import org.xbib.net.mail.util.TraceOutputStream; + +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 static final Logger logger = Logger.getLogger(Protocol.class.getName()); + + 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 String apopChallenge = null; + private Map capabilities = null; + private boolean pipelining; + private boolean noauthdebug = true; // hide auth info in debug output + 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, + Properties props, String prefix, boolean isSSL) + throws IOException { + this.host = host; + this.props = props; + this.prefix = prefix; + 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) { + if (socket != null) { + 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()); + traceInput.setQuote(quote); + traceOutput = new TraceOutputStream(socket.getOutputStream()); + 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, + StandardCharsets.ISO_8859_1)); + output = new PrintWriter( + new BufferedWriter( + new OutputStreamWriter(traceOutput, StandardCharsets.ISO_8859_1))); + } + + /** + * 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; + r = new BufferedReader(new InputStreamReader(in, StandardCharsets.US_ASCII)); + String s; + try (in) { + 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 + } + } + + /** + * 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; + + 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 (!r.ok) + return r.data != null ? r.data : "login failed"; + return null; + } + + /** + * 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(StandardCharsets.ISO_8859_1)); // XXX + } catch (NoSuchAlgorithmException nsae) { + 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; + } + + /** + * Run authentication query based on command and initial response + * + * @param command - command passed to server + * @param ir - initial response, part of the query + */ + protected void runAuthenticationCommand(String command, String ir) throws IOException { + if (logger.isLoggable(Level.FINE)) { + logger.fine(command + " using one line authentication format"); + } + + if (ir != null) { + resp = simpleCommand(command + " " + (ir.isEmpty() ? "=" : ir)); + } else { + resp = simpleCommand(command); + } + } + + /** + * 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); + runAuthenticationCommand("AUTH " + mech, ir); + + 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 (!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 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); + + 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 = Base64.getEncoder().encode( + resp.getBytes(StandardCharsets.UTF_8)); + return ASCIIUtility.toString(b); + } + + @Override + protected void runAuthenticationCommand(String command, String ir) throws IOException { + Boolean isTwoLineAuthenticationFormat = getBoolProp( + props, + prefix + ".auth.xoauth2.two.line.authentication.format"); + + if (isTwoLineAuthenticationFormat) { + if (logger.isLoggable(Level.FINE)) { + logger.fine(command + " using two line authentication format"); + } + + resp = twoLinesCommand( + command, + (ir.length() == 0 ? "=" : ir) + ); + } else { + super.runAuthenticationCommand(command, ir); + } + } + + @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 = Base64.getDecoder().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 | 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; + } + + /** + * Issue a two line POP3 command and return the response + * Refer to {@link #simpleCommand(String)} for a single line command + * + * @param firstCommand first command we want to pass to server e.g AUTH XOAUTH2 + * @param secondCommand second command e.g Base64 encoded authorization string + * @return Response + */ + private Response twoLinesCommand(String firstCommand, String secondCommand) throws IOException { + String cmd = firstCommand + " " + secondCommand; + + batchCommandStart(cmd); + simpleCommand(firstCommand); + batchCommandContinue(cmd); + + Response r = simpleCommand(secondCommand); + + batchCommandEnd(); + + 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) { + 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(); + } + + /* + * 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/net-mail/src/main/java/org/xbib/net/mail/pop3/Status.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/Status.java new file mode 100644 index 0000000..7297c0a --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/Status.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.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/net-mail/src/main/java/org/xbib/net/mail/pop3/TempFile.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/TempFile.java new file mode 100644 index 0000000..b87c434 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/TempFile.java @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.pop3; + +import java.io.File; +import java.io.IOException; + +/** + * 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(); + } + +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/pop3/WritableSharedFile.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/WritableSharedFile.java new file mode 100644 index 0000000..4007034 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/WritableSharedFile.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.pop3; + +import jakarta.mail.util.SharedFileInputStream; +import java.io.File; +import java.io.IOException; +import java.io.RandomAccessFile; + + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/pop3/package-info.java b/net-mail/src/main/java/org/xbib/net/mail/pop3/package-info.java new file mode 100644 index 0000000..178b755 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/pop3/package-info.java @@ -0,0 +1,821 @@ +/* + * Copyright (c) 2021, 2024 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 + */ + +/** + * 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 jakarta.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 org.xbib.net.mail.pop3.POP3Folder#getUID POP3Folder.getUID()}. + * You can use it as follows: + *

+ *
+ * if (folder instanceof org.xbib.net.mail.pop3.POP3Folder) {
+ * org.xbib.net.mail.pop3.POP3Folder pf =
+ * (org.xbib.net.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 org.xbib.net.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.executor.writetimeoutjava.util.concurrent.ScheduledExecutorService Provides specific ScheduledExecutorService for mail.pop3.writetimeout option. + * The value of mail.pop3.writetimeout shouldn't be a null. + * For provided executor pool it is highly recommended to have set up in true + * {@link java.util.concurrent.ScheduledThreadPoolExecutor#setRemoveOnCancelPolicy(boolean)}. + * Without it, write methods will create garbage that would only be reclaimed after the timeout. + * Be careful with calling {@link java.util.concurrent.ScheduledThreadPoolExecutor#shutdownNow()} in your executor, + * it can kill the running tasks. It would be ok to use shutdownNow only when JavaMail sockets are closed. + * This would be all service subclasses ({@link jakarta.mail.Store}/{@link jakarta.mail.Transport}) + * Invoking run {@link java.lang.Runnable#run()} on the returned {@link java.util.concurrent.Future} objects + * would force close the open connections. + * Instead of shutdownNow you can use {@link java.util.concurrent.ScheduledThreadPoolExecutor#shutdown()} ()} + * and + * {@link java.util.concurrent.ScheduledThreadPoolExecutor#awaitTermination(long, java.util.concurrent.TimeUnit)} ()}. + *
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 org.xbib.net.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.auth.xoauth2.two.line.authentication.formatbooleanIf true, splits authentication command on two lines. + * Default is false.
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 false, it does not check the server identity as specified by + * RFC 2595, + * RFC 2830, and + * RFC 6125. + * These additional checks based on the content of the server's certificate + * are intended to prevent man-in-the-middle attacks. + * Defaults to true. + *
mail.pop3.ssl.hostnameverifierjavax.net.ssl.HostnameVerifier + * If set to an object that implements the + * javax.net.ssl.HostnameVerifier interface then, this object + * will be used to verify the hostname against the certificate. Note that this + * is an instance of a class, not a name, and must be set using the + * put method, not the setProperty method. The given + * object will provide additional checks based on the content of the server's + * certificate are intended to prevent man-in-the-middle attacks. Defaults to + * null. + *
mail.pop3.ssl.hostnameverifier.classString + * If set, specifies the name of a class that implements the + * javax.net.ssl.HostnameVerifier interface or an alias name + * assigned to a built in hostname verifier. A class name will be instantiated + * using the default constructor and that instance will be used to verify the + * hostname against the certificate. The alias name "legacy" will + * enable the "sun.security.util.HostnameChecker" with fail over to + * the "MailHostnameVerifier". The alias name + * "sun.security.util.HostnameChecker" or + * "JdkHostnameChecker" will attempt to access the + * sun.security.util.HostnameChecker via reflection. The alias name + * "MailHostnameVerifier" will check server identity as specified + * by RFC 2595. + * The instantiated object will provide additional checks based on the content + * of the server's certificate are intended to prevent man-in-the-middle + * attacks. Defaults to null. + *
mail.pop3.ssl.trustString + * If set, and a socket factory hasn't been specified, enables use of a + * {@link org.xbib.net.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 org.xbib.net.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 org.xbib.net.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 + * jakarta.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 jakarta.mail.Session Session} configuration, + * the org.xbib.net.mail.pop3 provider logs the same information using + * {@link java.util.logging.Logger} as described in the following table: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
POP3 Loggers
Logger NameLogging LevelPurpose
org.xbib.net.mail.pop3CONFIGConfiguration of the POP3Store
org.xbib.net.mail.pop3FINEGeneral debugging output
org.xbib.net.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. + */ +package org.xbib.net.mail.pop3; diff --git a/net-mail/src/main/java/org/xbib/net/mail/remote/POP3RemoteProvider.java b/net-mail/src/main/java/org/xbib/net/mail/remote/POP3RemoteProvider.java new file mode 100644 index 0000000..cf5a19d --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/remote/POP3RemoteProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.remote; + +import jakarta.mail.Provider; + +/** + * The POP3 remote protocol provider. + */ +public class POP3RemoteProvider extends Provider { + public POP3RemoteProvider() { + super(Type.STORE, "pop3remote", + POP3RemoteStore.class.getName(), "Oracle", null); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/remote/POP3RemoteStore.java b/net-mail/src/main/java/org/xbib/net/mail/remote/POP3RemoteStore.java new file mode 100644 index 0000000..b8f6966 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/remote/POP3RemoteStore.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.remote; + +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.URLName; +import org.xbib.net.mail.pop3.POP3Store; + +/** + * A local store that uses POP3 to populate the INBOX. + * + * @author Bill Shannon + */ +public class POP3RemoteStore extends RemoteStore { + + public POP3RemoteStore(Session session, URLName url) { + super(session, url); + } + + protected Store getRemoteStore(Session session, URLName url) { + return new POP3Store(session, url); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/remote/RemoteDefaultFolder.java b/net-mail/src/main/java/org/xbib/net/mail/remote/RemoteDefaultFolder.java new file mode 100644 index 0000000..28d1b67 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/remote/RemoteDefaultFolder.java @@ -0,0 +1,52 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.remote; + +import jakarta.mail.Folder; +import jakarta.mail.Store; +import org.xbib.net.mail.mbox.MboxFolder; +import org.xbib.net.mail.mbox.MboxStore; + +/** + * The default folder for the "remote" protocol. + * + * @author Bill Shannon + */ +public class RemoteDefaultFolder extends MboxFolder { + + protected RemoteDefaultFolder(RemoteStore store, String name) { + super(store, name); + } + + /** + * Depending on the name of the requested folder, create an + * appropriate Folder subclass. If the name is + * null, create a RemoteDefaultFolder. + * If the name is "INBOX" (ignoring case), create a + * RemoteInbox. Otherwise, create an MboxFolder. + * + * @return the new Folder + */ + protected Folder createFolder(Store store, String name) { + if (name == null) + return new RemoteDefaultFolder((RemoteStore) store, null); + else if (name.equalsIgnoreCase("INBOX")) + return new RemoteInbox((RemoteStore) store, name); + else + return new MboxFolder((MboxStore) store, name); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/remote/RemoteInbox.java b/net-mail/src/main/java/org/xbib/net/mail/remote/RemoteInbox.java new file mode 100644 index 0000000..dc7138e --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/remote/RemoteInbox.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.remote; + +import jakarta.mail.MessagingException; +import org.xbib.net.mail.mbox.MboxFolder; + +/** + * A remote Inbox folder. The data is actually managed by our subclass + * (MboxFolder). We fetch data from the remote Inbox and + * add it to the local Inbox. + * + * @author Bill Shannon + */ + +public class RemoteInbox extends MboxFolder { + + private RemoteStore mstore; + + protected RemoteInbox(RemoteStore store, String name) { + super(store, name); + this.mstore = store; + } + + /** + * Poll the remote store for any new messages. + */ + public synchronized boolean hasNewMessages() { + try { + mstore.updateInbox(); + } catch (MessagingException ex) { + // ignore it + } + return super.hasNewMessages(); + } + + /** + * Open the folder in the specified mode. + * Poll the remote store for any new messages first. + */ + public synchronized void open(int mode) throws MessagingException { + mstore.updateInbox(); + super.open(mode); + } + + /** + * Return the number of messages in this folder. + * Poll the remote store for any new messages first. + */ + public synchronized int getMessageCount() throws MessagingException { + mstore.updateInbox(); + return super.getMessageCount(); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/remote/RemoteStore.java b/net-mail/src/main/java/org/xbib/net/mail/remote/RemoteStore.java new file mode 100644 index 0000000..e6695f4 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/remote/RemoteStore.java @@ -0,0 +1,137 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.remote; + +import jakarta.mail.Flags; +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.URLName; +import org.xbib.net.mail.mbox.MboxStore; + +/** + * A wrapper around a local MboxStore that fetches data + * from the Inbox in a remote store and adds it to our local Inbox. + */ +public abstract class RemoteStore extends MboxStore { + + protected Store remoteStore; + protected Folder remoteInbox; + protected Folder inbox; + protected String host, user, password; + protected int port; + protected long lastUpdate = 0; + + @SuppressWarnings("this-escape") + public RemoteStore(Session session, URLName url) { + super(session, url); + remoteStore = getRemoteStore(session, url); + } + + /** + * Subclasses override this method to return the appropriate + * Store object. This method will be called by + * the RemoteStore constructor. + */ + protected abstract Store getRemoteStore(Session session, URLName url); + + /** + * Connect to the store. + */ + @Override + public void connect(String host, int port, String user, String password) + throws MessagingException { + this.host = host; + this.port = port; + this.user = user; + this.password = password; + updateInbox(); + } + + /** + * Fetch any new mail in the remote INBOX and add it to the local INBOX. + */ + protected void updateInbox() throws MessagingException { + // is it time to do an update yet? + // XXX - polling frequency, rules, etc. should be in properties + if (System.currentTimeMillis() < lastUpdate + (5 * 1000)) + return; + try { + /* + * Connect to the remote store, using the saved + * authentication information. + */ + remoteStore.connect(host, port, user, password); + + /* + * If this store isn't connected yet, do it now, because + * it needs to be connected to get the INBOX folder. + */ + if (!isConnected()) + super.connect(host, port, user, password); + if (remoteInbox == null) + remoteInbox = remoteStore.getFolder("INBOX"); + if (inbox == null) + inbox = getFolder("INBOX"); + remoteInbox.open(Folder.READ_WRITE); + Message[] msgs = remoteInbox.getMessages(); + inbox.appendMessages(msgs); + remoteInbox.setFlags(msgs, new Flags(Flags.Flag.DELETED), true); + remoteInbox.close(true); + remoteStore.close(); + } catch (MessagingException ex) { + try { + if (remoteInbox != null && remoteInbox.isOpen()) + remoteInbox.close(false); + } finally { + if (remoteStore != null && remoteStore.isConnected()) + remoteStore.close(); + } + throw ex; + } + } + + @Override + public Folder getDefaultFolder() throws MessagingException { + checkConnected(); + + return new RemoteDefaultFolder(this, null); + } + + @Override + public Folder getFolder(String name) throws MessagingException { + checkConnected(); + + if (name.equalsIgnoreCase("INBOX")) + return new RemoteInbox(this, name); + else + return super.getFolder(name); + } + + @Override + public Folder getFolder(URLName url) throws MessagingException { + checkConnected(); + return getFolder(url.getFile()); + } + + private void checkConnected() throws MessagingException { + if (!isConnected()) + throw new MessagingException("Not connected"); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/smtp/DigestMD5.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/DigestMD5.java new file mode 100644 index 0000000..1f12695 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/DigestMD5.java @@ -0,0 +1,237 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.io.StreamTokenizer; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.HashMap; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; +import org.xbib.net.mail.util.ASCIIUtility; +import org.xbib.net.mail.util.BASE64DecoderStream; +import org.xbib.net.mail.util.BASE64EncoderStream; + +/** + * DIGEST-MD5 authentication support. + * + * @author Dean Gibson + * @author Bill Shannon + */ + +public class DigestMD5 { + + private static final Logger logger = Logger.getLogger(DigestMD5.class.getName()); + + private MessageDigest md5; + private String uri; + private String clientResponse; + + public DigestMD5() { + } + + /** + * Return client's authentication response to server's challenge. + * + * @return byte array with client's response + * @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 + * @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 ... + + 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=\"").append(user).append("\""); + result.append(",realm=\"").append(realm).append("\""); + result.append(",qop=").append(qop); + result.append(",nc=").append(nc); + result.append(",nonce=\"").append(nonce).append("\""); + result.append(",cnonce=\"").append(cnonce).append("\""); + result.append(",digest-uri=\"").append(uri).append("\""); + if (utf8) + result.append(",charset=\"utf-8\""); + result.append(",response=").append(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(StandardCharsets.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) + ), StandardCharsets.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 final 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/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPAddressFailedException.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPAddressFailedException.java new file mode 100644 index 0000000..87fb441 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPAddressFailedException.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import jakarta.mail.SendFailedException; +import jakarta.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 + */ +@SuppressWarnings("serial") +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 + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPAddressSucceededException.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPAddressSucceededException.java new file mode 100644 index 0000000..b92828e --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPAddressSucceededException.java @@ -0,0 +1,84 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import jakarta.mail.MessagingException; +import jakarta.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 + */ +@SuppressWarnings("serial") +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 + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPMessage.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPMessage.java new file mode 100644 index 0000000..bda9e44 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPMessage.java @@ -0,0 +1,333 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; +import java.io.InputStream; + +/** + * 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 org.xbib.net.mail.smtp package + * documentation for further information on the SMTP protocol provider.

+ * + * @author Bill Shannon + * @see 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 SMTPTransport SMTPTransport} + * method {@link 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/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPOutputStream.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPOutputStream.java new file mode 100644 index 0000000..9936bbb --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPOutputStream.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import java.io.IOException; +import java.io.OutputStream; +import org.xbib.net.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/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPProvider.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPProvider.java new file mode 100644 index 0000000..594bfee --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import jakarta.mail.Provider; +import org.xbib.net.mail.util.DefaultProvider; + +/** + * The SMTP protocol provider. + */ +@DefaultProvider // Remove this annotation if you copy this provider +public class SMTPProvider extends Provider { + public SMTPProvider() { + super(Type.TRANSPORT, "smtp", SMTPTransport.class.getName(), + "Oracle", null); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSSLProvider.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSSLProvider.java new file mode 100644 index 0000000..32ce55b --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSSLProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import jakarta.mail.Provider; +import org.xbib.net.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(Type.TRANSPORT, "smtps", + SMTPSSLTransport.class.getName(), "Oracle", null); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSSLTransport.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSSLTransport.java new file mode 100644 index 0000000..340a7aa --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSSLTransport.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import jakarta.mail.Session; +import jakarta.mail.URLName; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSaslAuthenticator.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSaslAuthenticator.java new file mode 100644 index 0000000..10ae69d --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSaslAuthenticator.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import jakarta.mail.MessagingException; +import java.util.Base64; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.sasl.RealmCallback; +import javax.security.sasl.RealmChoiceCallback; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; +import org.xbib.net.security.auth.OAuth2SaslClientFactory; +import org.xbib.net.mail.util.ASCIIUtility; + +/** + * This class contains a single method that does authentication using + * SASL. This is in a separate class so that it can be compiled with + * J2SE 1.5. Eventually it should be merged into SMTPTransport.java. + */ + +public class SMTPSaslAuthenticator implements SaslAuthenticator { + + private static final Logger logger = Logger.getLogger(SMTPSaslAuthenticator.class.getName()); + + private SMTPTransport pr; + private String name; + private Properties props; + private String host; + + /* + * This is a hack to initialize the OAUTH SASL provider just before, + * and only if, we might need it. This avoids the need for the user + * to initialize it explicitly, or manually configure the security + * providers file. + */ + static { + try { + OAuth2SaslClientFactory.init(); + } catch (Throwable t) { + } + } + + public SMTPSaslAuthenticator(SMTPTransport pr, String name, + Properties props, String host) { + this.pr = pr; + this.name = name; + this.props = props; + this.host = host; + } + + @Override + public boolean authenticate(String[] mechs, final String realm, + final String authzid, final String u, + final String p) throws MessagingException { + + boolean done = false; + if (logger.isLoggable(Level.FINE)) { + logger.fine("SASL Mechanisms:"); + for (int i = 0; i < mechs.length; i++) + logger.fine(" " + mechs[i]); + logger.fine(""); + } + + SaslClient sc; + CallbackHandler cbh = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL callback length: " + callbacks.length); + for (int i = 0; i < callbacks.length; i++) { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL callback " + i + ": " + callbacks[i]); + if (callbacks[i] instanceof NameCallback) { + NameCallback ncb = (NameCallback) callbacks[i]; + ncb.setName(u); + } else if (callbacks[i] instanceof PasswordCallback) { + PasswordCallback pcb = (PasswordCallback) callbacks[i]; + pcb.setPassword(p.toCharArray()); + } else if (callbacks[i] instanceof RealmCallback) { + RealmCallback rcb = (RealmCallback) callbacks[i]; + rcb.setText(realm != null ? + realm : rcb.getDefaultText()); + } else if (callbacks[i] instanceof RealmChoiceCallback) { + RealmChoiceCallback rcb = + (RealmChoiceCallback) callbacks[i]; + if (realm == null) + rcb.setSelectedIndex(rcb.getDefaultChoice()); + else { + // need to find specified realm in list + String[] choices = rcb.getChoices(); + for (int k = 0; k < choices.length; k++) { + if (choices[k].equals(realm)) { + rcb.setSelectedIndex(k); + break; + } + } + } + } + } + } + }; + + try { + @SuppressWarnings("unchecked") + Map propsMap = (Map) props; + sc = Sasl.createSaslClient(mechs, authzid, name, host, + propsMap, cbh); + } catch (SaslException sex) { + logger.log(Level.FINE, "Failed to create SASL client", sex); + throw new UnsupportedOperationException(sex.getMessage(), sex); + } + if (sc == null) { + logger.fine("No SASL support"); + throw new UnsupportedOperationException("No SASL support"); + } + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL client " + sc.getMechanismName()); + + int resp; + try { + String mech = sc.getMechanismName(); + String ir = null; + if (sc.hasInitialResponse()) { + byte[] ba = sc.evaluateChallenge(new byte[0]); + if (ba.length > 0) { + ba = Base64.getEncoder().encode(ba); + ir = ASCIIUtility.toString(ba, 0, ba.length); + } else + ir = "="; + } + if (ir != null) + resp = pr.simpleCommand("AUTH " + mech + " " + ir); + else + resp = pr.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) { + pr.startTLS(); + if (ir != null) + resp = pr.simpleCommand("AUTH " + mech + " " + ir); + else + resp = pr.simpleCommand("AUTH " + mech); + } + + if (resp == 235) + return true; // success already! + + if (resp != 334) + return false; + } catch (Exception ex) { + logger.log(Level.FINE, "SASL AUTHENTICATE Exception", ex); + return false; + } + + while (!done) { // loop till we are done + try { + if (resp == 334) { + byte[] ba = null; + if (!sc.isComplete()) { + ba = ASCIIUtility.getBytes(responseText(pr)); + if (ba.length > 0) + ba = Base64.getDecoder().decode(ba); + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL challenge: " + + ASCIIUtility.toString(ba, 0, ba.length) + " :"); + ba = sc.evaluateChallenge(ba); + } + if (ba == null) { + logger.fine("SASL: no response"); + resp = pr.simpleCommand(""); + } else { + if (logger.isLoggable(Level.FINE)) + logger.fine("SASL response: " + + ASCIIUtility.toString(ba, 0, ba.length) + " :"); + ba = Base64.getEncoder().encode(ba); + resp = pr.simpleCommand(ba); + } + } else + done = true; + } catch (Exception ioex) { + logger.log(Level.FINE, "SASL Exception", ioex); + done = true; + // XXX - ultimately return true??? + } + } + if (resp != 235) + return false; + + if (sc.isComplete() /*&& res.status == SUCCESS*/) { + String qop = (String) sc.getNegotiatedProperty(Sasl.QOP); + if (qop != null && (qop.equalsIgnoreCase("auth-int") || + qop.equalsIgnoreCase("auth-conf"))) { + // XXX - NOT SUPPORTED!!! + logger.fine( + "SASL Mechanism requires integrity or confidentiality"); + return false; + } + } + + return true; + } + + private static final String responseText(SMTPTransport pr) { + String resp = pr.getLastServerResponse().trim(); + if (resp.length() > 4) + return resp.substring(4); + else + return ""; + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSendFailedException.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSendFailedException.java new file mode 100644 index 0000000..4fde586 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSendFailedException.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import jakarta.mail.Address; +import jakarta.mail.SendFailedException; +import jakarta.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 + */ +@SuppressWarnings("serial") +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 + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSenderFailedException.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSenderFailedException.java new file mode 100644 index 0000000..5729c9b --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPSenderFailedException.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import jakarta.mail.SendFailedException; +import jakarta.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 + */ +@SuppressWarnings("serial") +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 + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPTransport.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPTransport.java new file mode 100644 index 0000000..c0f481b --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SMTPTransport.java @@ -0,0 +1,2784 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import jakarta.mail.Address; +import jakarta.mail.AuthenticationFailedException; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.SendFailedException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.URLName; +import jakarta.mail.event.TransportEvent; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.internet.MimePart; +import jakarta.mail.internet.ParseException; +import jakarta.mail.util.StreamProvider.EncoderTypes; +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StringReader; +import java.lang.reflect.Constructor; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Base64; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.logging.Level; +import java.util.logging.Logger; +import javax.net.ssl.SSLSocket; +import org.xbib.net.security.auth.Ntlm; +import org.xbib.net.mail.util.ASCIIUtility; +import org.xbib.net.mail.util.BASE64EncoderStream; +import org.xbib.net.mail.util.LineInputStream; +import org.xbib.net.mail.util.MailConnectException; +import org.xbib.net.mail.util.PropUtil; +import org.xbib.net.mail.util.SocketConnectException; +import org.xbib.net.mail.util.SocketFetcher; +import org.xbib.net.mail.util.TraceInputStream; +import org.xbib.net.mail.util.TraceOutputStream; + +/** + * This class implements the Transport abstract class using SMTP for + * message submission and transport.

+ * + * See the org.xbib.net.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 jakarta.mail.event.ConnectionEvent + * @see TransportEvent + */ + +public class SMTPTransport extends Transport { + + private static final Logger logger = Logger.getLogger(SMTPTransport.class.getName()); + + 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 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(); + + 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 SMTPAddressSucceededException + * SMTPAddressSucceededException} will be included in the exception + * chain for each successful address, along with the usual + * {@link 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). + * + * @throws MessagingException for non-authentication failures + * @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 + */ + @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 (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 (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(Base64.getEncoder().encode( + user.getBytes(StandardCharsets.UTF_8))); + if (resp == 334) { + // send passwd + resp = simpleCommand(Base64.getEncoder().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(); + 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); + + 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 = Base64.getEncoder().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( + "org.xbib.net.mail.smtp.SMTPSaslAuthenticator"); + Constructor c = sac.getConstructor(SMTPTransport.class, + String.class, + Properties.class, + String.class); + saslAuthenticator = (SaslAuthenticator) c.newInstance( + new Object[]{ + this, + name, + session.getProperties(), + 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[0]); + return saslAuthenticator.authenticate(mechs, realm, authzid, u, p); + } + + /** + * 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 + * @throws SMTPSendFailedException if the send failed because of + * an SMTP command error + * @throws SendFailedException if the send failed because of + * invalid addresses. + * @throws MessagingException if the connection is dead + * or not in the connected state or if the message is + * not a MimeMessage. + * @see TransportEvent + */ + @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(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder()) || + enc.equalsIgnoreCase(EncoderTypes.BASE_64.getEncoder()))) { + 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", EncoderTypes.BIT8_ENCODER.getEncoder()); + 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 | MessagingException ioex) { + // 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; + } + + + private BufferedInputStream serverInput; + private LineInputStream lineInputStream; + private OutputStream serverOutput; + private Socket serverSocket; + private TraceInputStream traceInput; + private TraceOutputStream traceOutput; + + /** + * 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 2xy, 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 if (retCode >= 200 && retCode <= 299) { + // see RFC 5321 section 4.3.2 + // assume address is valid, although we don't really know + 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); + } 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()); + traceInput.setQuote(quote); + traceOutput = new TraceOutputStream(serverSocket.getOutputStream()); + traceOutput.setQuote(quote); + serverOutput = new BufferedOutputStream(traceOutput); + serverInput = new BufferedInputStream(traceInput); + lineInputStream = new LineInputStream(serverInput); + } + + /** + * 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 | StringIndexOutOfBoundsException nfe) { + 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/net-mail/src/main/java/org/xbib/net/mail/smtp/SaslAuthenticator.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/SaslAuthenticator.java new file mode 100644 index 0000000..258b4ba --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/SaslAuthenticator.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.smtp; + +import jakarta.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/net-mail/src/main/java/org/xbib/net/mail/smtp/package-info.java b/net-mail/src/main/java/org/xbib/net/mail/smtp/package-info.java new file mode 100644 index 0000000..d255832 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/smtp/package-info.java @@ -0,0 +1,882 @@ +/* + * Copyright (c) 2021, 2024 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 + */ + +/** + * 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 org.xbib.net.mail.smtp.SMTPAddressFailedException SMTPAddressFailedException} + * chained off the top level + * {@link jakarta.mail.SendFailedException SendFailedException} + * that is thrown. + * In addition, if the mail.smtp.reportsuccess property + * is set, an + * {@link org.xbib.net.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 jakarta.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 org.xbib.net.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 + * {@link 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.executor.writetimeoutjava.util.concurrent.ScheduledExecutorService Provides specific ScheduledExecutorService for mail.smtp.writetimeout option. + * The value of mail.smtp.writetimeout shouldn't be a null. + * For provided executor pool it is highly recommended to have set up in true + * {@link java.util.concurrent.ScheduledThreadPoolExecutor#setRemoveOnCancelPolicy(boolean)}. + * Without it, write methods will create garbage that would only be reclaimed after the timeout. + * Be careful with calling {@link java.util.concurrent.ScheduledThreadPoolExecutor#shutdownNow()} in your executor, + * it can kill the running tasks. It would be ok to use shutdownNow only when JavaMail sockets are closed. + * This would be all service subclasses ({@link jakarta.mail.Store}/{@link jakarta.mail.Transport}) + * Invoking run {@link java.lang.Runnable#run()} on the returned {@link java.util.concurrent.Future} objects + * would force close the open connections. + * Instead of shutdownNow you can use {@link java.util.concurrent.ScheduledThreadPoolExecutor#shutdown()} ()} + * and + * {@link java.util.concurrent.ScheduledThreadPoolExecutor#awaitTermination(long, java.util.concurrent.TimeUnit)} ()}. + *
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 org.xbib.net.mail.smtp.SMTPMessage#setSubmitter setSubmitter} + * method of {@link org.xbib.net.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 org.xbib.net.mail.smtp.SMTPAddressSucceededException + * SMTPAddressSucceededException} + * for each address that is successful. + * Note also that this will cause a + * {@link jakarta.mail.SendFailedException SendFailedException} + * to be thrown from the + * {@link org.xbib.net.mail.smtp.SMTPTransport#sendMessage sendMessage} + * method of + * {@link org.xbib.net.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 false, it does not check the server identity as specified by + * RFC 2595, + * RFC 2830, and + * RFC 6125. + * These additional checks based on the content of the server's certificate + * are intended to prevent man-in-the-middle attacks. + * Defaults to true. + *
mail.smtp.ssl.hostnameverifierjavax.net.ssl.HostnameVerifier + * If set to an object that implements the + * javax.net.ssl.HostnameVerifier interface then, this object + * will be used to verify the hostname against the certificate. Note that this + * is an instance of a class, not a name, and must be set using the + * put method, not the setProperty method. The given + * object will provide additional checks based on the content of the server's + * certificate are intended to prevent man-in-the-middle attacks. Defaults to + * null. + *
mail.smtp.ssl.hostnameverifier.classString + * If set, specifies the name of a class that implements the + * javax.net.ssl.HostnameVerifier interface or an alias name + * assigned to a built in hostname verifier. A class name will be instantiated + * using the default constructor and that instance will be used to verify the + * hostname against the certificate. The alias name "legacy" will + * enable the "sun.security.util.HostnameChecker" with fail over to + * the "MailHostnameVerifier". The alias name + * "sun.security.util.HostnameChecker" or + * "JdkHostnameChecker" will attempt to access the + * sun.security.util.HostnameChecker via reflection. The alias name + * "MailHostnameVerifier" will check server identity as specified + * by RFC 2595. + * The instantiated object will provide additional checks based on the content + * of the server's certificate are intended to prevent man-in-the-middle + * attacks. Defaults to null. + *
mail.smtp.ssl.trustString + * If set, and a socket factory hasn't been specified, enables use of a + * {@link org.xbib.net.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 org.xbib.net.mail.smtp.SMTPTransport SMTPTransport} + * method {@link org.xbib.net.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 jakarta.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 jakarta.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 + * jakarta.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 jakarta.mail.Session Session} configuration, + * the org.xbib.net.mail.smtp provider logs the same information using + * {@link java.util.logging.Logger} as described in the following table: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
SMTP Loggers
Logger NameLogging LevelPurpose
org.xbib.net.mail.smtpCONFIGConfiguration of the SMTPTransport
org.xbib.net.mail.smtpFINEGeneral debugging output
org.xbib.net.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. + */ +package org.xbib.net.mail.smtp; diff --git a/net-mail/src/main/java/org/xbib/net/mail/util/ASCIIUtility.java b/net-mail/src/main/java/org/xbib/net/mail/util/ASCIIUtility.java new file mode 100644 index 0000000..fff147d --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/ASCIIUtility.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +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 + * @throws 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 + * @throws 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 + * @throws 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 + * @throws 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/net-mail/src/main/java/org/xbib/net/mail/util/BASE64DecoderStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/BASE64DecoderStream.java new file mode 100644 index 0000000..71adcb9 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/BASE64DecoderStream.java @@ -0,0 +1,408 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.EOFException; +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * 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 { + /** + * 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; + } + + // 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. + * @throws 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. + * @throws 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)); + } + + /** + * 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 + * @throws 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 = Math.min(input_pos, 10); + 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; + } + + /*** 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/net-mail/src/main/java/org/xbib/net/mail/util/BASE64EncoderStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/BASE64EncoderStream.java new file mode 100644 index 0000000..2e73b31 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/BASE64EncoderStream.java @@ -0,0 +1,293 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * 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 { + /** + * 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 + }; + private static byte[] newline = new byte[]{'\r', '\n'}; + 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; + + /** + * 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); + } + + /** + * 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; + } + + /** + * 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. + * @throws 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. + * @throws 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. + * @throws 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. + * + * @throws 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(); + } + + /** + * Encode the data stored in buffer. + * Uses outbuf to store the encoded + * data before writing. + * + * @throws 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; + } + } + + /*** 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/net-mail/src/main/java/org/xbib/net/mail/util/BEncoderStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/BEncoderStream.java new file mode 100644 index 0000000..40d6dac --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/BEncoderStream.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.OutputStream; + +/** + * 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 + } + +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/util/CRLFOutputStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/CRLFOutputStream.java new file mode 100644 index 0000000..f851b5b --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/CRLFOutputStream.java @@ -0,0 +1,90 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + + +/** + * Convert lines into the canonical format, that is, terminate lines with the + * CRLF sequence. + * + * @author John Mani + */ +public class CRLFOutputStream extends FilterOutputStream { + private static final byte[] newline = {(byte) '\r', (byte) '\n'}; + protected int lastb = -1; + protected boolean atBOL = true; // at beginning of line? + + 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/net-mail/src/main/java/org/xbib/net/mail/util/DecodingException.java b/net-mail/src/main/java/org/xbib/net/mail/util/DecodingException.java new file mode 100644 index 0000000..f13308e --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/DecodingException.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.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 + */ +@SuppressWarnings("serial") +public class DecodingException extends IOException { + + /** + * Constructor. + * + * @param s the exception message + */ + public DecodingException(String s) { + super(s); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/util/DefaultProvider.java b/net-mail/src/main/java/org/xbib/net/mail/util/DefaultProvider.java new file mode 100644 index 0000000..1dbae90 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/DefaultProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019, 2023 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 org.xbib.net.mail.util; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/util/LineInputStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/LineInputStream.java new file mode 100644 index 0000000..2ca8622 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/LineInputStream.java @@ -0,0 +1,175 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; +import java.nio.ByteBuffer; +import java.nio.charset.CharacterCodingException; +import java.nio.charset.CharsetDecoder; +import java.nio.charset.CodingErrorAction; +import java.nio.charset.StandardCharsets; + +/** + * 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 implements jakarta.mail.util.LineInputStream { + + private static boolean defaultutf8 = + PropUtil.getBooleanSystemProperty("mail.mime.allowutf8", false); + private static int MAX_INCR = 1024 * 1024; // 1MB + private boolean allowutf8; + private byte[] lineBuffer = null; // reusable byte buffer + private CharsetDecoder decoder; + + 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 + * @throws IOException for I/O errors + */ + @SuppressWarnings("deprecation") // for old String constructor + @Override + 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/net-mail/src/main/java/org/xbib/net/mail/util/LineOutputStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/LineOutputStream.java new file mode 100644 index 0000000..b177ad6 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/LineOutputStream.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +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 implements jakarta.mail.util.LineOutputStream { + private static byte[] newline; + + static { + newline = new byte[2]; + newline[0] = (byte) '\r'; + newline[1] = (byte) '\n'; + } + + private boolean allowutf8; + + 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; + } + + @Override + 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); + } + + @Override + public void writeln() throws IOException { + out.write(newline); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/util/LogOutputStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/LogOutputStream.java new file mode 100644 index 0000000..f2ea70e --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/LogOutputStream.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2008, 2023 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 org.xbib.net.mail.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Capture output lines and send them to the mail logger. + */ +public class LogOutputStream extends OutputStream { + + private static final Logger logger = Logger.getLogger(LogOutputStream.class.getName()); + + private final Level level; + + private int lastb = -1; + private byte[] buf = new byte[80]; + private int pos = 0; + + public LogOutputStream() { + 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/net-mail/src/main/java/org/xbib/net/mail/util/MailConnectException.java b/net-mail/src/main/java/org/xbib/net/mail/util/MailConnectException.java new file mode 100644 index 0000000..1367a73 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/MailConnectException.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.util; + +import jakarta.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. + * + * @author Bill Shannon + * @see java.net.ConnectException + * @since JavaMail 1.5.0 + */ +@SuppressWarnings("serial") +public class MailConnectException extends MessagingException { + private final String host; + private final int port; + private final int cto; + + /** + * Constructs a MailConnectException. + * + * @param cex the SocketConnectException with the details + * @throws NullPointerException if given exception is {@code null}. + */ + @SuppressWarnings("this-escape") + 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(); + super.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/net-mail/src/main/java/org/xbib/net/mail/util/MailSSLSocketFactory.java b/net-mail/src/main/java/org/xbib/net/mail/util/MailSSLSocketFactory.java new file mode 100644 index 0000000..5c63e03 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/MailSSLSocketFactory.java @@ -0,0 +1,373 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Arrays; +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * 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. + * + * @author Stephan Sann + * @author Bill Shannon + * @since JavaMail 1.4.2 + */ +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. + */ + public MailSSLSocketFactory() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + 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 + */ + public MailSSLSocketFactory(String protocol) throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + + // 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 + */ + public synchronized void setKeyManagers(KeyManager... keyManagers) throws KeyManagementException { + this.keyManagers = keyManagers.clone(); + newAdapteeFactory(); + } + + /** + * @return the secureRandom + */ + public synchronized SecureRandom getSecureRandom() { + return secureRandom; + } + + /** + * @param secureRandom the secureRandom to set + */ + public synchronized void setSecureRandom(SecureRandom secureRandom) throws KeyManagementException { + this.secureRandom = secureRandom; + newAdapteeFactory(); + } + + /** + * @return the trustManagers + */ + public synchronized TrustManager[] getTrustManagers() { + return trustManagers; + } + + /** + * @param trustManagers the trustManagers to set + */ + public synchronized void setTrustManagers(TrustManager... trustManagers) throws KeyManagementException { + 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 sslSocket SSLSocket connected to the server + * @param server name of the server we connected to + * @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 NoSuchAlgorithmException, KeyStoreException { + 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/net-mail/src/main/java/org/xbib/net/mail/util/MailStreamProvider.java b/net-mail/src/main/java/org/xbib/net/mail/util/MailStreamProvider.java new file mode 100644 index 0000000..2e0a9a7 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/MailStreamProvider.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2021 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 org.xbib.net.mail.util; + +import jakarta.mail.util.SharedByteArrayInputStream; +import jakarta.mail.util.StreamProvider; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * Contains the required encoders/decoders and streams required by the API. + */ +public class MailStreamProvider implements StreamProvider { + + /** + * Public constructor. + */ + public MailStreamProvider() { + } + + @Override + public InputStream inputBase64(InputStream in) { + return new BASE64DecoderStream(in); + } + + @Override + public OutputStream outputBase64(OutputStream out) { + return new BASE64EncoderStream(out); + } + + @Override + public InputStream inputBinary(InputStream in) { + return in; + } + + @Override + public OutputStream outputBinary(OutputStream out) { + return out; + } + + @Override + public OutputStream outputB(OutputStream out) { + return new BEncoderStream(out); + } + + @Override + public InputStream inputQ(InputStream in) { + return new QDecoderStream(in); + } + + @Override + public OutputStream outputQ(OutputStream out, boolean encodingWord) { + return new QEncoderStream(out, encodingWord); + } + + @Override + public LineInputStream inputLineStream(InputStream in, boolean allowutf8) { + return new LineInputStream(in, allowutf8); + } + + @Override + public LineOutputStream outputLineStream(OutputStream out, boolean allowutf8) { + return new LineOutputStream(out, allowutf8); + } + + @Override + public InputStream inputQP(InputStream in) { + return new QPDecoderStream(in); + } + + @Override + public OutputStream outputQP(OutputStream out) { + return new QPEncoderStream(out); + } + + @Override + public InputStream inputSharedByteArray(byte[] bytes) { + return new SharedByteArrayInputStream(bytes); + } + + @Override + public InputStream inputUU(InputStream in) { + return new UUDecoderStream(in); + } + + @Override + public OutputStream outputUU(OutputStream out, String filename) { + if (filename == null) { + return new UUEncoderStream(out); + } else { + return new UUEncoderStream(out, filename); + } + } + +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/util/PropUtil.java b/net-mail/src/main/java/org/xbib/net/mail/util/PropUtil.java new file mode 100644 index 0000000..514f3ad --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/PropUtil.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.util.Properties; +import java.util.concurrent.ScheduledExecutorService; + +/** + * 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 a ScheduledExecutorService valued property. + * + * @param props the properties + * @param name the property name + * @return the property value, null if the property is null + * @throws ClassCastException if the property value's class is + * not {@link java.util.concurrent.ScheduledThreadPoolExecutor } + */ + public static ScheduledExecutorService getScheduledExecutorServiceProperty(Properties props, + String name) { + return getScheduledExecutorService(getProp(props, name)); + } + + /** + * 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) { + return getBoolean(getProp(System.getProperties(), name), 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; + return def; + } + + private static ScheduledExecutorService getScheduledExecutorService(Object value) { + return (ScheduledExecutorService) value; + } + + /** + * Interpret the value object as a boolean, + * returning def if unable. + */ + private static boolean getBoolean(Object value, boolean def) { + switch (value) { + case null -> { + return def; + } + case String s -> { + /* + * 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"); + } + case Boolean b -> { + return b; + } + default -> { + } + } + return def; + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/util/QDecoderStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/QDecoderStream.java new file mode 100644 index 0000000..1b913ac --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/QDecoderStream.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.IOException; +import java.io.InputStream; + +/** + * 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. + * @throws 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/net-mail/src/main/java/org/xbib/net/mail/util/QEncoderStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/QEncoderStream.java new file mode 100644 index 0000000..ec83537 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/QEncoderStream.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * 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 static final String WORD_SPECIALS = "=_?\"#$%&'(),.:;<>@[\\]^`{|}~"; + private static final String TEXT_SPECIALS = "=_?"; + private String 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. + * @throws 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); + } + + /**** 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/net-mail/src/main/java/org/xbib/net/mail/util/QPDecoderStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/QPDecoderStream.java new file mode 100644 index 0000000..b3f8de0 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/QPDecoderStream.java @@ -0,0 +1,201 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PushbackInputStream; + +/** + * 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. + * @throws 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. + * @throws 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/net-mail/src/main/java/org/xbib/net/mail/util/QPEncoderStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/QPEncoderStream.java new file mode 100644 index 0000000..eee37dc --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/QPEncoderStream.java @@ -0,0 +1,202 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * 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 { + // The encoding table + private final static char[] hex = { + '0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' + }; + 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. + * @throws 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. + * @throws 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. + * @throws 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. + * + * @throws 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. + * + * @throws 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; + } + + 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/net-mail/src/main/java/org/xbib/net/mail/util/ReadableMime.java b/net-mail/src/main/java/org/xbib/net/mail/util/ReadableMime.java new file mode 100644 index 0000000..6e41178 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/ReadableMime.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, 2023 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 org.xbib.net.mail.util; + +import jakarta.mail.MessagingException; +import java.io.InputStream; + +/** + * 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 + * @throws MessagingException for failures + */ + public InputStream getMimeStream() throws MessagingException; +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/util/SharedByteArrayOutputStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/SharedByteArrayOutputStream.java new file mode 100644 index 0000000..0ce3fc4 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/SharedByteArrayOutputStream.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2012, 2023 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 org.xbib.net.mail.util; + +import jakarta.mail.util.SharedByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/util/SocketConnectException.java b/net-mail/src/main/java/org/xbib/net/mail/util/SocketConnectException.java new file mode 100644 index 0000000..1090592 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/SocketConnectException.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.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. + * + * @author Bill Shannon + * @see java.net.ConnectException + * @since JavaMail 1.5.0 + */ +@SuppressWarnings("serial") +public class SocketConnectException extends IOException { + /** + * The socket host name. + */ + private final String host; + /** + * The socket port. + */ + private final int port; + /** + * The connection timeout. + */ + private final int cto; + + /** + * 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, cause); + this.host = host; + this.port = port; + this.cto = cto; + } + + /** + * The exception that caused the failure. + * + * @return the exception + */ + public Exception getException() { + Throwable t = super.getCause(); + 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/net-mail/src/main/java/org/xbib/net/mail/util/SocketFetcher.java b/net-mail/src/main/java/org/xbib/net/mail/util/SocketFetcher.java new file mode 100644 index 0000000..7a52317 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/SocketFetcher.java @@ -0,0 +1,1156 @@ +/* + * Copyright (c) 1997, 2024 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 org.xbib.net.mail.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintStream; +import java.io.UncheckedIOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.UndeclaredThrowableException; +import java.net.ConnectException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketTimeoutException; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.text.MessageFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Properties; +import java.util.StringTokenizer; +import java.util.concurrent.ScheduledExecutorService; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import javax.net.SocketFactory; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; + +/** + * 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 final Logger logger = Logger.getLogger(SocketFetcher.class.getName()); + + // 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 + * @throws 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); + ScheduledExecutorService executorService = PropUtil.getScheduledExecutorServiceProperty(props, + prefix + ".executor.writetimeout"); + socket = executorService == null ? + new WriteTimeoutSocket(socket, writeTimeout) : + new WriteTimeoutSocket(socket, writeTimeout, executorService); + } + 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 (NoSuchAlgorithmException | KeyStoreException | KeyManagementException gex) { + IOException ioex = new IOException("Can't create MailSSLSocketFactory"); + 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.isEmpty()) + return null; + + // dynamically load the class + + ClassLoader cl = Thread.currentThread().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" + ); + SocketFactory sf = (SocketFactory) mthGetDefault.invoke(new Object(), new Object[]{}); + return sf; + } + + /** + * 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 + * @throws 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 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 (NoSuchAlgorithmException | KeyStoreException | KeyManagementException 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[0])); + } + 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())); + } + + try { + /* + * Check server identity and trust. + * See: JDK-8062515 and JDK-7192189 + * LDAPS requires the same regex handling as we need + */ + String eia = PropUtil.getBooleanProperty(props, + prefix + ".ssl.checkserveridentity", true) + ? "LDAPS" : (String) null; + SSLParameters params = sslsocket.getSSLParameters(); + params.setEndpointIdentificationAlgorithm(eia); + sslsocket.setSSLParameters(params); + + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "Using endpoint identification " + + "algorithm {0} with SNIs {1} for: {2}", + new Object[]{eia, Objects.toString( + params.getServerNames()), host}); + } + } catch (RuntimeException re) { + throw cleanupAndThrow(sslsocket, + new IOException("Unable to set endpoint identification " + + "algorithm for: " + host, re)); + } + + /* + * 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. + */ + try { + sslsocket.startHandshake(); + } catch (IOException ioe) { + throw cleanupAndThrow(sslsocket, ioe); + } + + /* + * Check server identity and trust with user provided checks. + */ + try { + checkServerIdentity(getHostnameVerifier(props, prefix), + host, sslsocket); + } catch (IOException | ReflectiveOperationException + | RuntimeException | LinkageError re) { + throw cleanupAndThrow(sslsocket, + new IOException("Unable to check server identity for: " + + host, re)); + } + + 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) + * using the given HostnameVerifier. All hostname verifier implementations + * are allowed to throw unchecked exceptions. + * + * @param hnv the HostnameVerifier or null if allowing all. + * @param server name of the server expected + * @param sslSocket SSLSocket connected to the server. Caller is expected + * to close the socket on error. + * @throws IOException if we can't verify identity of server + * @throws RuntimeException caused by invoking the verifier + */ + private static void checkServerIdentity(HostnameVerifier hnv, + String server, SSLSocket sslSocket) + throws IOException { + if (logger.isLoggable(Level.FINER)) { + //Only expose the toString of the HostnameVerifier to the logger + //and not a direct reference to the HostnameVerifier + logger.log(Level.FINER, MessageFormat.format( + "Using HostnameVerifier {0} with {1}, {2}", + Objects.toString(hnv), server, + Objects.toString(sslSocket))); + } + + if (hnv == null) { + return; + } + + // Check against the server name(s) as expressed in server certificate + if (!hnv.verify(server, sslSocket.getSession())) { + throw new SSLException("Server identity does not match " + + "authentication scheme: " + hnv + " DENY"); + } + } + + private static X509Certificate getX509Certificate( + java.security.cert.Certificate[] certChain) throws IOException { + if (certChain == null || certChain.length == 0) { + throw new SSLPeerUnverifiedException( + Arrays.toString(certChain)); + } + + java.security.cert.Certificate first = certChain[0]; + if (first instanceof X509Certificate) { + return (X509Certificate) first; + } + + //Only metadata about the cert is shown in the message + throw new SSLPeerUnverifiedException(first == null ? "null" + : (first.getClass().getName() + " " + first.getType())); + } + + + /** + * Return an instance of {@link HostnameVerifier}. + *

+ * This method assumes the {@link HostnameVerifier} class provides an + * accessible default constructor to instantiate the instance. + * + * @return the {@link HostnameVerifier} or null + * @throws ClassCastException if hostnameverifier is not a {@link HostnameVerifier} + * @throws ReflectiveOperationException if unable to construct a {@link HostnameVerifier} + */ + private static HostnameVerifier getHostnameVerifier(Properties props, String prefix) + throws ReflectiveOperationException { + + //Custom object is used before factory. + HostnameVerifier hvn = (HostnameVerifier) + props.get(prefix + ".ssl.hostnameverifier"); + if (hvn != null) { + return hvn; + } + + String fqcn = props.getProperty(prefix + ".ssl.hostnameverifier.class"); + if (fqcn == null || fqcn.isEmpty()) { + return null; + } + + //Handle all aliases names + if ("legacy".equals(fqcn)) { + return JdkHostnameChecker.ofFailover(MailHostnameVerifier.of()); + } + + if ("sun.security.util.HostnameChecker".equals(fqcn) + || JdkHostnameChecker.class.getSimpleName().equals(fqcn)) { + return JdkHostnameChecker.of(); + } + + if (MailHostnameVerifier.class.getSimpleName().equals(fqcn)) { + return MailHostnameVerifier.of(); + } + + //Ensure an alias is never loaded from modulepath or classpath. + //This ensures that removed aliases never loads a weaker verifier and + //that all future aliases, classes without package names, are reserved + //for future use. + if (fqcn.indexOf('.') < 0) { + throw new ClassNotFoundException(fqcn); + } + + //Handle the fully qualified class name + Class verifierClass = null; + try { + verifierClass = Class.forName(fqcn) + .asSubclass(HostnameVerifier.class); + } catch (ClassNotFoundException | RuntimeException cnfe) { + logger.log(Level.FINER, + "Calling class loader could not find: " + fqcn, cnfe); + } + + //Try system class loader + if (verifierClass == null) { + try { + verifierClass = Class.forName(fqcn, false, + ClassLoader.getSystemClassLoader()) + .asSubclass(HostnameVerifier.class); + } catch (ClassNotFoundException | RuntimeException cnfe) { + logger.log(Level.FINER, + "System class loader could not find: " + fqcn, cnfe); + } + } + + if (verifierClass != null) { + return verifierClass.getConstructor().newInstance(); + } + + //Everything failed, try to expose one of the reasons. + verifierClass = Class.forName(fqcn) + .asSubclass(HostnameVerifier.class); + //This is unexpected, should be unreachable + throw new ClassNotFoundException(fqcn, + new IllegalStateException(verifierClass.toString())); + } + + /** + * 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( + Base64.getEncoder().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(); + StringBuilder errorLine = new StringBuilder(); + if (!readProxyResponse(socket.getInputStream(), errorLine)) { + try { + socket.close(); + } catch (IOException ioex) { + // ignored + } + ConnectException ex = new ConnectException( + "connection through proxy " + + proxyHost + ":" + proxyPort + " to " + + host + ":" + port + " failed: " + errorLine.toString()); + logger.log(Level.FINE, "connect failed", ex); + throw ex; + } + } + + public static boolean readProxyResponse(InputStream input, StringBuilder errorLine) throws IOException { + LineInputStream r = new LineInputStream(input, true); + + String line; + boolean first = true; + while ((line = r.readLine()) != null) { + if (line.length() == 0) { + // End of HTTP response + break; + } + logger.finest(line); + if (first) { + StringTokenizer st = new StringTokenizer(line); + String http = st.nextToken(); + String code = st.nextToken(); + if (!code.equals("200")) { + errorLine.append(line); + return false; + } + first = false; + } + } + return true; + } + + /** + * 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[0]); + } + + /** + * Check the server from the Socket connection against the server name(s) + * as expressed in the server certificate (RFC 2595 check). + *

+ * We implement a crude version of the same checks ourselves. + *

+ * The verify method will throw unchecked exceptions instead of returning + * false. This violation of specification is acceptable because this class + * is private and doesn't escape the SocketFetcher. SocketFetcher is able + * to safely handle unchecked exceptions from HostnameVerifier::verify. + */ + private static final class MailHostnameVerifier implements HostnameVerifier { + + private static final HostnameVerifier OF = new MailHostnameVerifier(); + + private MailHostnameVerifier() { + } + + static HostnameVerifier of() { + return OF; + } + + /** + * 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); + } + } + + /** + * Verify that the host name is an acceptable match. + * + * @param server the host name. + * @param ssls the SSLSession + * @return true if matches. + * @throws UncheckedIOException if the SSL peer is unverified. + * @throws UndeclaredThrowableException wrapping the checked exception. + */ + @Override + public boolean verify(String server, SSLSession ssls) { + Objects.requireNonNull(server); + X509Certificate cert = null; + try { + cert = getX509Certificate(ssls.getPeerCertificates()); + if (logger.isLoggable(Level.FINER)) { + logger.finer("matchCert server " + + server + ", cert " + cert); + } + /* + * 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 (List nameEnt : names) { + final int type = (Integer) nameEnt.get(0); + if (type == 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 + throw new UndeclaredThrowableException( + new CertificateException( + "No subject alternative DNS name matching " + + server + " found"), toString() + " DENY"); + } + } + } catch (CertificateParsingException ignore) { + logger.log(Level.FINEST, server, ignore); + } catch (IOException spue) { + throw new UncheckedIOException(toString() + " DENY", spue); + } + + if (cert == null) { + throw new UncheckedIOException(toString() + " DENY", + new SSLPeerUnverifiedException("null")); + } + + // 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; + + throw new UndeclaredThrowableException(new CertificateException( + "No name matching " + server + " found"), toString() + " DENY"); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } + } + + /** + * Check the server from the Socket connection against the server name(s) + * as expressed in the server certificate (RFC 2595 check). This is a + * reflective adapter class for the sun.security.util.HostnameChecker, + * which exists in Sun's JDK starting with 1.4.1. Validation is using LDAPS + * (RFC 2830) host name checking. + *

+ * The verify method will throw unchecked exceptions instead of returning + * false. This violation of specification is acceptable because this class + * is private and doesn't escape the SocketFetcher. SocketFetcher is able + * to safely handle unchecked exceptions from HostnameVerifier::verify. + *

+ * This class will print --illegal-access=warn console warnings on JDK9 + * and may require: -add-opens 'java.base/sun.security.util=ALL-UNNAMED' + * or --add-opens 'java.base/sun.security.util=jakarta.mail' depending on + * how this class has been packaged. + * It is preferred to set mail..ssl.endpointidentitycheck property + * to 'LDAPS' instead of using this verifier. This adapter will be removed + * in a future release of Angus Mail when there is no reason to keep this + * for compatibility sake. When this is removed ensure that the `legacy` + * alias doesn't search the modulepath/classpath as that would be a security + * risk. + *

+ * See: JDK-8062515 - Migrate use of sun.security.** to supported API + */ + private static final class JdkHostnameChecker implements HostnameVerifier { + private final HostnameVerifier failover; + + private JdkHostnameChecker(final HostnameVerifier or) { + this.failover = Objects.requireNonNull(or); + } + + static HostnameVerifier of() { + return ofFailover((n, s) -> { + return false; + }); + } + + static HostnameVerifier ofFailover(HostnameVerifier or) { + //Making factory methods return a singleton is pointless because + //this class should only be used as a last resort. + return new JdkHostnameChecker(or); + } + + /** + * Verify that the host name is an acceptable match. + * + * @param server the host name. + * @param ssls the SSLSession + * @return true if matches. + * @throws UncheckedIOException if the SSL peer is unverified. + * @throws UndeclaredThrowableException wrapping the checked exception. + */ + @Override + public boolean verify(String server, SSLSession ssls) { + try { + X509Certificate cert = getX509Certificate( + ssls.getPeerCertificates()); + + if (logger.isLoggable(Level.FINER)) { + logger.finer("matchCert server " + + server + ", cert " + cert); + } + + 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", + byte.class); + Object hostnameChecker = getInstance + .invoke((Object) null, (byte) 2); + + // invoke hostnameChecker.match( server, cert) + logger.finer("using sun.security.util.HostnameChecker"); + Method match = hnc.getMethod("match", + String.class, X509Certificate.class); + try { + match.invoke(hostnameChecker, server, cert); + return true; + } catch (InvocationTargetException ite) { + Throwable ce = ite.getCause(); + if (ce instanceof CertificateException) { + logger.log(Level.FINER, "HostnameChecker DENY", ite); + throw new UndeclaredThrowableException(ce, toString() + " DENY"); + } + throw ite; //treat as fail over + } + } catch (IOException | ReflectiveOperationException roe) { + logger.log(Level.FINER, "HostnameChecker FAIL", roe); + try { + if (failover.verify(server, ssls)) { + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, "allowed by: {0}", + failover.toString()); + } + return true; + } + } catch (Throwable fail) { + if (logger.isLoggable(Level.FINER)) { + logger.log(Level.FINER, toString() + + "FAIL -> THROW", fail); + } + if (fail != roe) + fail.addSuppressed(roe); + throw fail; + } + + //Report real reason rather than just failing to verify + Throwable t = roe; + if (roe instanceof InvocationTargetException) + t = roe.getCause(); + if (t == null) //Broken subclass + t = roe; + if (t instanceof RuntimeException) + throw (RuntimeException) t; + if (t instanceof Error) + throw (Error) t; + + String msg = toString() + " FAIL -> DENY"; + if (t instanceof IOException) + throw new UncheckedIOException(msg, (IOException) t); + throw new UndeclaredThrowableException(t, msg); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + " -> " + failover; + } + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/util/TimeoutOutputStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/TimeoutOutputStream.java new file mode 100644 index 0000000..5e926da --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/TimeoutOutputStream.java @@ -0,0 +1,123 @@ +package org.xbib.net.mail.util; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.Socket; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * 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. + */ +public class TimeoutOutputStream extends OutputStream { + private static final String WRITE_TIMEOUT_MESSAGE = "Write timed out"; + private static final String CANNOT_GET_TIMEOUT_TASK_RESULT_MESSAGE = "Couldn't get result of timeout task"; + + private final OutputStream os; + private final ScheduledExecutorService ses; + private final Callable timeoutTask; + private final int timeout; + private final Socket socket; + private byte[] b1; + // Implement timeout with a scheduled task + private ScheduledFuture sf = null; + + public TimeoutOutputStream(Socket socket, ScheduledExecutorService ses, int timeout) throws IOException { + this.os = socket.getOutputStream(); + this.ses = ses; + this.timeout = timeout; + this.socket = socket; + timeoutTask = new Callable() { + @Override + public String call() throws Exception { + try { + os.close(); // close the stream to abort the write + } catch (Throwable t) { + return t.toString(); + } + return WRITE_TIMEOUT_MESSAGE; + } + }; + } + + @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; + } + + try { + try { + if (timeout > 0) + sf = ses.schedule(timeoutTask, + timeout, TimeUnit.MILLISECONDS); + } catch (RejectedExecutionException ex) { + if (!socket.isClosed()) { + throw new IOException("Write aborted due to timeout not enforced", ex); + } + } + + try { + os.write(bs, off, len); + } catch (IOException e) { + if (sf != null && !sf.cancel(true)) { + throw new IOException(handleTimeoutTaskResult(sf), e); + } + throw e; + } + } finally { + if (sf != null) + sf.cancel(true); + } + } + + @Override + public void close() throws IOException { + os.close(); + if (sf != null) { + sf.cancel(true); + } + } + + private String handleTimeoutTaskResult(ScheduledFuture sf) { + boolean wasInterrupted = Thread.interrupted(); + String exceptionMessage = null; + try { + return sf.get(timeout, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + exceptionMessage = String.format("%s %s", e, ses.toString()); + } catch (InterruptedException e) { + wasInterrupted = true; + exceptionMessage = e.toString(); + } catch (ExecutionException e) { + exceptionMessage = e.getCause() == null ? e.toString() : e.getCause().toString(); + } catch (Exception e) { + exceptionMessage = e.toString(); + } finally { + if (wasInterrupted) { + Thread.currentThread().interrupt(); + } + } + + return String.format("%s. %s", CANNOT_GET_TIMEOUT_TASK_RESULT_MESSAGE, exceptionMessage); + } +} diff --git a/net-mail/src/main/java/org/xbib/net/mail/util/TraceInputStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/TraceInputStream.java new file mode 100644 index 0000000..668759e --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/TraceInputStream.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 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 static Logger logger = Logger.getLogger(TraceInputStream.class.getName()); + + 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. + */ + public TraceInputStream(InputStream in) { + super(in); + this.trace = logger.isLoggable(Level.FINEST); + this.traceOut = new LogOutputStream(); + } + + /** + * 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/net-mail/src/main/java/org/xbib/net/mail/util/TraceOutputStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/TraceOutputStream.java new file mode 100644 index 0000000..0b63b41 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/TraceOutputStream.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * 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 static final Logger logger = Logger.getLogger(TraceOutputStream.class.getName()); + + 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. + */ + public TraceOutputStream(OutputStream out) { + super(out); + this.trace = logger.isLoggable(Level.FINEST); + this.traceOut = new LogOutputStream(); + } + + /** + * 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 + * @throws 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 + * @throws 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/net-mail/src/main/java/org/xbib/net/mail/util/UUDecoderStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/UUDecoderStream.java new file mode 100644 index 0000000..e78aa07 --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/UUDecoderStream.java @@ -0,0 +1,338 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.FilterInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * 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. + * @throws 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 + * @throws 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 + * @throws 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/net-mail/src/main/java/org/xbib/net/mail/util/UUEncoderStream.java b/net-mail/src/main/java/org/xbib/net/mail/util/UUEncoderStream.java new file mode 100644 index 0000000..d2b374d --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/UUEncoderStream.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.util; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.PrintStream; + +/** + * 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/net-mail/src/main/java/org/xbib/net/mail/util/WriteTimeoutSocket.java b/net-mail/src/main/java/org/xbib/net/mail/util/WriteTimeoutSocket.java new file mode 100644 index 0000000..71e71fe --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/WriteTimeoutSocket.java @@ -0,0 +1,371 @@ +/* + * Copyright (c) 2013, 2023 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 org.xbib.net.mail.util; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketAddress; +import java.net.SocketException; +import java.net.SocketOption; +import java.nio.channels.SocketChannel; +import java.util.Collections; +import java.util.Set; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +/** + * 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; + // flag to indicate whether scheduled executor is provided from outside or + // should be created here in constructor + private final boolean isExternalSes; + // 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 = createScheduledThreadPool(); + this.isExternalSes = false; + this.timeout = timeout; + } + + public WriteTimeoutSocket(Socket socket, int timeout, ScheduledExecutorService ses) throws IOException { + this.socket = socket; + this.ses = ses; + this.timeout = timeout; + this.isExternalSes = true; + } + + 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, ses, timeout); + } + + @Override + public boolean getTcpNoDelay() throws SocketException { + return socket.getTcpNoDelay(); + } + + @Override + public void setTcpNoDelay(boolean on) throws SocketException { + socket.setTcpNoDelay(on); + } + + @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 boolean getOOBInline() throws SocketException { + return socket.getOOBInline(); + } + + @Override + public void setOOBInline(boolean on) throws SocketException { + socket.setOOBInline(on); + } + + @Override + public int getSoTimeout() throws SocketException { + return socket.getSoTimeout(); + } + + @Override + public void setSoTimeout(int timeout) throws SocketException { + socket.setSoTimeout(timeout); + } + + @Override + public int getSendBufferSize() throws SocketException { + return socket.getSendBufferSize(); + } + + @Override + public void setSendBufferSize(int size) throws SocketException { + socket.setSendBufferSize(size); + } + + @Override + public int getReceiveBufferSize() throws SocketException { + return socket.getReceiveBufferSize(); + } + + @Override + public void setReceiveBufferSize(int size) throws SocketException { + socket.setReceiveBufferSize(size); + } + + @Override + public boolean getKeepAlive() throws SocketException { + return socket.getKeepAlive(); + } + + @Override + public void setKeepAlive(boolean on) throws SocketException { + socket.setKeepAlive(on); + } + + @Override + public int getTrafficClass() throws SocketException { + return socket.getTrafficClass(); + } + + @Override + public void setTrafficClass(int tc) throws SocketException { + socket.setTrafficClass(tc); + } + + @Override + public boolean getReuseAddress() throws SocketException { + return socket.getReuseAddress(); + } + + @Override + public void setReuseAddress(boolean on) throws SocketException { + socket.setReuseAddress(on); + } + + @Override + public void close() throws IOException { + try { + socket.close(); + } finally { + if (!isExternalSes) + 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$() { + //The loop handles issues with non-public classes between + //java.net.Socket and the actual socket type held in this object. + //Must inspect java.net.Socket to ensure compatiblity with old behavior. + for (Class k = socket.getClass(); k != Object.class; k = k.getSuperclass()) { + try { + Method m = k.getDeclaredMethod("getFileDescriptor$"); + if (FileDescriptor.class.isAssignableFrom(m.getReturnType())) { + //Skip setAccessible so non-public methods fail to invoke. + return (FileDescriptor) m.invoke(socket); + } + } catch (Exception ignore) { + } + } + return null; + } + + private ScheduledThreadPoolExecutor createScheduledThreadPool() { + ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1); + // Without setting setRemoveOnCancelPolicy = true write methods will create garbage that would only be + // reclaimed after the timeout. + scheduledThreadPoolExecutor.setRemoveOnCancelPolicy(true); + return scheduledThreadPoolExecutor; + } +} + + diff --git a/net-mail/src/main/java/org/xbib/net/mail/util/package-info.java b/net-mail/src/main/java/org/xbib/net/mail/util/package-info.java new file mode 100644 index 0000000..2a0b2ac --- /dev/null +++ b/net-mail/src/main/java/org/xbib/net/mail/util/package-info.java @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * 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: + *

+ * + * + * + * + * + * + * + * + * + * + * + * + * + *
org.xbib.net.mail.util Loggers
Logger NameLogging LevelPurpose
org.xbib.net.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. + */ +package org.xbib.net.mail.util; diff --git a/net-mail/src/main/resources/META-INF/javamail.address.map b/net-mail/src/main/resources/META-INF/javamail.address.map new file mode 100644 index 0000000..4ab5572 --- /dev/null +++ b/net-mail/src/main/resources/META-INF/javamail.address.map @@ -0,0 +1 @@ +rfc822=smtp diff --git a/net-mail/src/main/resources/META-INF/javamail.charset.map b/net-mail/src/main/resources/META-INF/javamail.charset.map new file mode 100644 index 0000000..e933270 --- /dev/null +++ b/net-mail/src/main/resources/META-INF/javamail.charset.map @@ -0,0 +1,78 @@ +### JDK-to-MIME charset mapping table #### +### This should be the first mapping table ### + +8859_1 ISO-8859-1 +iso8859_1 ISO-8859-1 +ISO8859-1 ISO-8859-1 + +8859_2 ISO-8859-2 +iso8859_2 ISO-8859-2 +ISO8859-2 ISO-8859-2 + +8859_3 ISO-8859-3 +iso8859_3 ISO-8859-3 +ISO8859-3 ISO-8859-3 + +8859_4 ISO-8859-4 +iso8859_4 ISO-8859-4 +ISO8859-4 ISO-8859-4 + +8859_5 ISO-8859-5 +iso8859_5 ISO-8859-5 +ISO8859-5 ISO-8859-5 + +8859_6 ISO-8859-6 +iso8859_6 ISO-8859-6 +ISO8859-6 ISO-8859-6 + +8859_7 ISO-8859-7 +iso8859_7 ISO-8859-7 +ISO8859-7 ISO-8859-7 + +8859_8 ISO-8859-8 +iso8859_8 ISO-8859-8 +ISO8859-8 ISO-8859-8 + +8859_9 ISO-8859-9 +iso8859_9 ISO-8859-9 +ISO8859-9 ISO-8859-9 + +SJIS Shift_JIS +JIS ISO-2022-JP +ISO2022JP ISO-2022-JP +EUC_JP euc-jp +KOI8_R koi8-r +EUC_CN euc-cn +EUC_TW euc-tw +EUC_KR euc-kr + +--DIVIDER: this line *must* start with "--" and end with "--" -- + +#### XXX-to-JDK charset mapping table #### + +iso-2022-cn ISO2022CN +iso-2022-kr ISO2022KR +utf-8 UTF8 +utf8 UTF8 +en_US.iso885915 ISO-8859-15 +ja_jp.iso2022-7 ISO2022JP +ja_jp.eucjp EUCJIS + +# these two are not needed in 1.1.6. (since EUC_KR exists +# and KSC5601 will map to the correct converter) +euc-kr KSC5601 +euckr KSC5601 + +# in JDK 1.1.6 we will no longer need the "us-ascii" convert +us-ascii ISO-8859-1 +x-us-ascii ISO-8859-1 + +# Chinese charsets are a mess and widely misrepresented. +# gb18030 is a superset of gbk, which is a supserset of cp936/ms936, +# which is a superset of gb2312. +# https://bugzilla.gnome.org/show_bug.cgi?id=446783 +# map all of these to gb18030. +gb2312 GB18030 +cp936 GB18030 +ms936 GB18030 +gbk GB18030 diff --git a/net-mail/src/main/resources/META-INF/javamail.default.address.map b/net-mail/src/main/resources/META-INF/javamail.default.address.map new file mode 100644 index 0000000..4ab5572 --- /dev/null +++ b/net-mail/src/main/resources/META-INF/javamail.default.address.map @@ -0,0 +1 @@ +rfc822=smtp diff --git a/net-mail/src/main/resources/META-INF/javamail.providers b/net-mail/src/main/resources/META-INF/javamail.providers new file mode 100644 index 0000000..cf2148b --- /dev/null +++ b/net-mail/src/main/resources/META-INF/javamail.providers @@ -0,0 +1,4 @@ +protocol=pop3; type=store; class=org.xbib.net.mail.pop3.POP3Store; vendor=xbib; +protocol=pop3s; type=store; class=org.xbib.net.mail.pop3.POP3SSLStore; vendor=xbib; +protocol=smtp; type=transport; class=org.xbib.net.mail.smtp.SMTPTransport; vendor=xbib; +protocol=smtps; type=transport; class=org.xbib.net.mail.smtp.SMTPSSLTransport; vendor=xbib; diff --git a/net-mail/src/main/resources/META-INF/mailcap b/net-mail/src/main/resources/META-INF/mailcap new file mode 100644 index 0000000..16e58a3 --- /dev/null +++ b/net-mail/src/main/resources/META-INF/mailcap @@ -0,0 +1,5 @@ +text/plain;; x-java-content-handler=org.xbib.net.mail.handlers.text_plain +text/html;; x-java-content-handler=org.xbib.net.mail.handlers.text_html +text/xml;; x-java-content-handler=org.xbib.net.mail.handlers.text_xml +multipart/*;; x-java-content-handler=org.xbib.net.mail.handlers.multipart_mixed; x-java-fallback-entry=true +message/rfc822;; x-java-content-handler=org.xbib.net.mail.handlers.message_rfc822 diff --git a/net-mail/src/main/resources/META-INF/services/jakarta.mail.Provider b/net-mail/src/main/resources/META-INF/services/jakarta.mail.Provider new file mode 100644 index 0000000..2f8a6a6 --- /dev/null +++ b/net-mail/src/main/resources/META-INF/services/jakarta.mail.Provider @@ -0,0 +1,4 @@ +org.xbib.net.mail.pop3.POP3Provider +org.xbib.net.mail.pop3.POP3SSLProvider +org.xbib.net.mail.smtp.SMTPProvider +org.xbib.net.mail.smtp.SMTPSSLProvider diff --git a/net-mail/src/main/resources/META-INF/services/jakarta.mail.util.StreamProvider b/net-mail/src/main/resources/META-INF/services/jakarta.mail.util.StreamProvider new file mode 100644 index 0000000..45c1577 --- /dev/null +++ b/net-mail/src/main/resources/META-INF/services/jakarta.mail.util.StreamProvider @@ -0,0 +1 @@ +org.xbib.net.mail.util.MailStreamProvider \ No newline at end of file diff --git a/net-mail/src/test/java/module-info.java b/net-mail/src/test/java/module-info.java new file mode 100644 index 0000000..ddd9fda --- /dev/null +++ b/net-mail/src/test/java/module-info.java @@ -0,0 +1,26 @@ +module org.xbib.net.mail.test { + requires java.logging; + requires java.security.sasl; + requires java.xml; + requires org.junit.jupiter.api; + requires org.junit.jupiter.params; + requires org.xbib.net.mail; + exports org.xbib.net.mail.test.handlers; + exports org.xbib.net.mail.test.iap; + exports org.xbib.net.mail.test.imap; + exports org.xbib.net.mail.test.imap.protocol; + exports org.xbib.net.mail.test.pop3; + exports org.xbib.net.mail.test.smtp; + exports org.xbib.net.mail.test.stream; + exports org.xbib.net.mail.test.test; + exports org.xbib.net.mail.test.util; + opens org.xbib.net.mail.test.handlers to org.junit.platform.commons; + opens org.xbib.net.mail.test.iap to org.junit.platform.commons; + opens org.xbib.net.mail.test.imap to org.junit.platform.commons; + opens org.xbib.net.mail.test.imap.protocol to org.junit.platform.commons; + opens org.xbib.net.mail.test.pop3 to org.junit.platform.commons; + opens org.xbib.net.mail.test.smtp to org.junit.platform.commons; + opens org.xbib.net.mail.test.stream to org.junit.platform.commons; + opens org.xbib.net.mail.test.test to org.junit.platform.commons; + opens org.xbib.net.mail.test.util to org.junit.platform.commons; +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/handlers/TextXmlTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/handlers/TextXmlTest.java new file mode 100644 index 0000000..c02d4d1 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/handlers/TextXmlTest.java @@ -0,0 +1,127 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.handlers; + +import jakarta.activation.ActivationDataFlavor; +import jakarta.activation.DataContentHandler; +import jakarta.activation.DataSource; +import jakarta.mail.util.ByteArrayDataSource; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.handlers.text_xml; + +import javax.xml.transform.stream.StreamSource; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test the text/xml DataContentHandler. + * + * XXX - should test other Source objects in addition to StreamSource. + * + * @author Bill Shannon + */ + +class TextXmlTest { + + private static String xml = "bar\n"; + + private static byte[] xmlBytes = xml.getBytes(); + + // test InputStream to String + @Test + public void testStreamToStringTextXml() throws Exception { + testStreamToString("text/xml"); + } + + // test InputStream to String + @Test + public void testStreamToStringApplicationXml() throws Exception { + testStreamToString("application/xml"); + } + + private static void testStreamToString(String mimeType) throws Exception { + DataContentHandler dch = new text_xml(); + ActivationDataFlavor df = new ActivationDataFlavor(String.class, mimeType, "XML"); + DataSource ds = new ByteArrayDataSource(xmlBytes, mimeType); + Object content = dch.getContent(ds); + assertEquals(String.class, content.getClass()); + assertEquals(xml, (String) content); + content = dch.getTransferData(df, ds); + assertEquals(String.class, content.getClass()); + assertEquals(xml, (String) content); + } + + // test InputStream to StreamSource + @Test + public void testStreamToSource() throws Exception { + DataContentHandler dch = new text_xml(); + ActivationDataFlavor df = new ActivationDataFlavor(StreamSource.class, + "text/xml", "XML stream"); + DataSource ds = new ByteArrayDataSource(xmlBytes, "text/xml"); + Object content = dch.getTransferData(df, ds); + assertEquals(StreamSource.class, content.getClass()); + String sc = streamToString(((StreamSource) content).getInputStream()); + assertEquals(xml, sc); + } + + // test String to OutputStream + @Test + public void testStringToStream() throws Exception { + DataContentHandler dch = new text_xml(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + dch.writeTo(xml, "text/xml", bos); + String sc = new String(bos.toByteArray(), StandardCharsets.US_ASCII); + assertEquals(xml, sc); + } + + // test StreamSource to OutputStream + @Test + public void testSourceToStream() throws Exception { + DataContentHandler dch = new text_xml(); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + StreamSource ss = new StreamSource(new ByteArrayInputStream(xmlBytes)); + dch.writeTo(ss, "text/xml", bos); + String sc = new String(bos.toByteArray(), StandardCharsets.US_ASCII); + // transformer adds an header, so can't check for exact match + assertTrue(sc.contains(xml.trim())); + } + + /** + * Read a stream into a String. + */ + private static String streamToString(InputStream is) { + try { + StringBuilder sb = new StringBuilder(); + int c; + while ((c = is.read()) > 0) + sb.append((char) c); + return sb.toString(); + } catch (IOException ex) { + return ""; + } finally { + try { + is.close(); + } catch (IOException cex) { + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/iap/ProtocolTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/iap/ProtocolTest.java new file mode 100644 index 0000000..c5e5651 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/iap/ProtocolTest.java @@ -0,0 +1,322 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.iap; + +import org.xbib.net.mail.iap.Protocol; +import org.xbib.net.mail.iap.ProtocolException; +import org.xbib.net.mail.test.test.NullOutputStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.PrintStream; +import java.lang.reflect.Method; +import java.net.Socket; +import java.nio.channels.SocketChannel; +import java.util.Properties; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test the Protocol class. + */ +final class ProtocolTest { + + private static final byte[] noBytes = new byte[0]; + private static final PrintStream nullps = + new PrintStream(new NullOutputStream()); + private static final ByteArrayInputStream nullis = + new ByteArrayInputStream(noBytes); + + /** + * Test that the tag prefix is computed properly. + */ + @Test + public void testTagPrefix() throws IOException, ProtocolException { + Protocol.tagNum.set(0); // reset for testing + String tag = newProtocolTag(); + assertEquals("A0", tag); + for (int i = 1; i < 26; i++) + tag = newProtocolTag(); + assertEquals("Z0", tag); + tag = newProtocolTag(); + assertEquals("AA0", tag); + for (int i = 26 + 1; i < (26 * 26 + 26); i++) + tag = newProtocolTag(); + assertEquals("ZZ0", tag); + tag = newProtocolTag(); + assertEquals("AAA0", tag); + for (int i = 26 * 26 + 26 + 1; i < (26 * 26 * 26 + 26 * 26 + 26); i++) + tag = newProtocolTag(); + assertEquals("ZZZ0", tag); + tag = newProtocolTag(); + // did it wrap around? + assertEquals("A0", tag); + } + + private String newProtocolTag() throws IOException, ProtocolException { + Properties props = new Properties(); + Protocol p = new Protocol(nullis, nullps, props, false); + String tag = p.writeCommand("CMD", null); + return tag; + } + + /** + * Test that the tag prefix is reused. + */ + @Test + public void testTagPrefixReuse() throws IOException, ProtocolException { + Properties props = new Properties(); + props.setProperty("mail.imap.reusetagprefix", "true"); + Protocol p = new Protocol(nullis, nullps, props, false); + String tag = p.writeCommand("CMD", null); + assertEquals("A0", tag); + p = new Protocol(nullis, nullps, props, false); + tag = p.writeCommand("CMD", null); + assertEquals("A0", tag); + } + + @Test + public void testLayer1Socket() throws IOException { + try (LayerAbstractSocket s = new Layer1of5()) { + findSocketChannel(s); + assertTrue(s.foundChannel()); + } + } + + @Test + public void testLayer2Socket() throws IOException { + try (LayerAbstractSocket s = new Layer2of5()) { + findSocketChannel(s); + assertTrue(s.foundChannel()); + } + } + + @Test + public void testLayer3Socket() throws IOException { + try (LayerAbstractSocket s = new Layer3of5()) { + findSocketChannel(s); + assertTrue(s.foundChannel()); + } + } + + @Test + public void testLayer4Socket() throws IOException { + try (LayerAbstractSocket s = new Layer4of5()) { + findSocketChannel(s); + assertTrue(s.foundChannel()); + } + } + + @Test + public void testLayer5Socket() throws IOException, ProtocolException { + try (LayerAbstractSocket s = new Layer5of5()) { + findSocketChannel(s); + assertTrue(s.foundChannel()); + } + } + + + @Test + public void testRenamed1Socket() throws IOException, ProtocolException { + try (RenamedAbstractSocket s = new RenamedSocketLayer1of3()) { + findSocketChannel(s); + assertTrue(s.foundChannel()); + } + } + + @Test + public void testRenamed2Socket() throws IOException, ProtocolException { + try (RenamedAbstractSocket s = new RenamedSocketLayer2of3()) { + findSocketChannel(s); + assertTrue(s.foundChannel()); + } + } + + @Test + public void testRenamed3Socket() throws IOException, ProtocolException { + try (RenamedAbstractSocket s = new RenamedSocketLayer3of3()) { + findSocketChannel(s); + assertTrue(s.foundChannel()); + } + } + + @Test + public void testNullSocketsRenamed() throws IOException, ProtocolException { + try (RenamedAbstractSocket s = new NullSocketsRenamedSocket()) { + findSocketChannel(s); + assertTrue(s.foundChannel()); + } + } + + @Test + public void testHidden1Socket() throws IOException, ProtocolException { + try (HiddenAbstractSocket s = new HiddenSocket1of2()) { + //This could be implemented to find the socket. + //However, we would have fetch field value to inspect the object. + //Most reads will not be what we are looking for so best to give up. + //Feel free to change the policy later if needed. + findSocketChannel(s); + assertFalse(s.foundChannel()); + } + } + + @Test + public void testHidden2Socket() throws IOException, ProtocolException { + try (HiddenAbstractSocket s = new HiddenSocket2of2()) { + //This could be implemented to find the socket. + //However, we would have fetch field value to inspect the object. + //Most reads will not be what we are looking for so best to give up. + //Feel free to change the policy later if needed. + findSocketChannel(s); + assertFalse(s.foundChannel()); + } + } + + @Test + public void testNamedNullAndHiddenSocket() throws IOException { + try (HiddenAbstractSocket s = new NamedNullAndHiddenSocket()) { + findSocketChannel(s); + assertFalse(s.foundChannel()); + } + } + + @Timeout(10) + @Test + public void testSelfNamedSocket() throws IOException { + try (WrappedSocket s = new SelfNamedSocket()) { + findSocketChannel(s); + assertFalse(WrappedSocket.foundChannel(s)); + } + } + + @Timeout(10) + @Test + public void testSelfHiddenSocket() throws IOException { + try (WrappedSocket s = new SelfHiddenSocket()) { + findSocketChannel(s); + assertFalse(WrappedSocket.foundChannel(s)); + } + } + + private SocketChannel findSocketChannel(Socket s) throws IOException { + try { + Method m = Protocol.class.getDeclaredMethod("findSocketChannel", Socket.class); + m.setAccessible(true); + return (SocketChannel) m.invoke(null, s); + } catch (RuntimeException re) { + throw re; + } catch (Exception e) { + throw new IOException(e); + } + } + + private static class RenamedSocketLayer3of3 extends RenamedSocketLayer1of3 { + } + + private static class RenamedSocketLayer2of3 extends RenamedSocketLayer1of3 { + } + + private static class RenamedSocketLayer1of3 extends RenamedAbstractSocket { + } + + private static abstract class RenamedAbstractSocket extends Socket { + private final Socket tekcos = new WrappedSocket(); + + public boolean foundChannel() { + return WrappedSocket.foundChannel(tekcos); + } + } + + private static class NullSocketsRenamedSocket extends RenamedAbstractSocket { + @SuppressWarnings("unused") + private Socket socket; + @SuppressWarnings("unused") + private Socket tekcos; + + } + + private static class Layer5of5 extends Layer4of5 { + } + + private static class Layer4of5 extends Layer3of5 { + } + + private static class Layer3of5 extends Layer2of5 { + } + + private static class Layer2of5 extends Layer1of5 { + } + + private static class Layer1of5 extends LayerAbstractSocket { + } + + private static abstract class LayerAbstractSocket extends Socket { + private final Socket socket = new WrappedSocket(); + + public boolean foundChannel() { + return WrappedSocket.foundChannel(socket); + } + } + + private static class SelfNamedSocket extends WrappedSocket { + @SuppressWarnings("unused") + private final Socket socket = this; + } + + private static class SelfHiddenSocket extends WrappedSocket { + @SuppressWarnings("unused") + private final Socket hidden = this; + } + + + private static class HiddenSocket2of2 extends HiddenSocket1of2 { + } + + private static class HiddenSocket1of2 extends HiddenAbstractSocket { + } + + private static abstract class HiddenAbstractSocket extends Socket { + private final Object hidden = new WrappedSocket(); + + public boolean foundChannel() { + return WrappedSocket.foundChannel(hidden); + } + } + + private static class NamedNullAndHiddenSocket extends HiddenAbstractSocket { + + @SuppressWarnings("unused") + private Socket socket; + } + + private static class WrappedSocket extends Socket { + private boolean found; + + public static boolean foundChannel(Object ws) { + return ws instanceof WrappedSocket && ((WrappedSocket) ws).found; + } + + @Override + public SocketChannel getChannel() { + found = true; + return null; + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/iap/ResponseInputStreamTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/iap/ResponseInputStreamTest.java new file mode 100644 index 0000000..072fc9b --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/iap/ResponseInputStreamTest.java @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2013, 2023 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 org.xbib.net.mail.test.iap; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.iap.ResponseInputStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test ResponseInputStream. + */ +public class ResponseInputStreamTest { + + /** + * Test that an EOF while reading a literal throws an IOException. + */ + @Test + public void testEofWhileReadingLiteral() throws Exception { + ByteArrayInputStream bis = new ByteArrayInputStream( + "test{1}\r\n".getBytes(StandardCharsets.ISO_8859_1)); + ResponseInputStream ris = new ResponseInputStream(bis); + try { + ris.readResponse(); + } catch (IOException ex) { + // success! + return; + } + fail("no exception"); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/iap/ResponseTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/iap/ResponseTest.java new file mode 100644 index 0000000..fa014f9 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/iap/ResponseTest.java @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2013, 2023 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 org.xbib.net.mail.test.iap; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.iap.Response; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test response parsing. + */ +@Timeout(5) +class ResponseTest { + + private static String[] atomTests = { + "atom", "atom ", "atom(", "atom)", "atom{", "atom*", "atom%", + "atom\"", "atom\\ ", "atom]", "atom\001", "atom\177" + }; + + private static String[] astringTests = { + "atom", "atom ", "atom(", "atom)", "atom{", "atom*", "atom%", + "atom\"", "atom\\ ", "atom\001", "atom\177", "\"atom\"", + "{4}\r\natom" + }; + + /** + * Test parsing atoms. + */ + @Test + public void testAtom() throws Exception { + for (String s : atomTests) { + Response r = new Response("* " + s); + assertEquals("atom", r.readAtom()); + } + for (String s : atomTests) { + Response r = new Response("* " + s + " "); + assertEquals("atom", r.readAtom()); + } + } + + /** + * Test parsing astrings. + */ + @Test + public void testAString() throws Exception { + for (String s : astringTests) { + Response r = new Response("* " + s); + assertEquals("atom", r.readAtomString()); + } + for (String s : astringTests) { + Response r = new Response("* " + s + " "); + assertEquals("atom", r.readAtomString()); + } + } + + /** + * Test the special case where an astring can include ']'. + */ + @Test + public void testAStringSpecial() throws Exception { + Response r = new Response("* " + "atom] "); + assertEquals("atom]", r.readAtomString()); + } + + /** + * Test astring lists. + */ + @Test + public void testAStringList() throws Exception { + Response r = new Response("* " + "(A B C)"); + assertArrayEquals(new String[]{"A", "B", "C"}, + r.readAtomStringList()); + } + + @Test + public void testAStringListInitialSpace() throws Exception { + Response r = new Response("* " + "( A B C)"); + assertArrayEquals(new String[]{"A", "B", "C"}, + r.readAtomStringList()); + } + + @Test + public void testAStringListTrailingSpace() throws Exception { + Response r = new Response("* " + "(A B C )"); + assertArrayEquals(new String[]{"A", "B", "C"}, + r.readAtomStringList()); + } + + @Test + public void testAStringListInitialAndTrailingSpace() throws Exception { + Response r = new Response("* " + "( A B C )"); + assertArrayEquals(new String[]{"A", "B", "C"}, + r.readAtomStringList()); + } + + @Test + public void testAStringListMultipleSpaces() throws Exception { + Response r = new Response("* " + "(A B C)"); + assertArrayEquals(new String[]{"A", "B", "C"}, + r.readAtomStringList()); + } + + @Test + public void testAStringListQuoted() throws Exception { + Response r = new Response("* " + "(A B \"C\")"); + assertArrayEquals(new String[]{"A", "B", "C"}, + r.readAtomStringList()); + } + + /** + * Test astring lists with more data following. + */ + @Test + public void testAStringListMore() throws Exception { + Response r = new Response("* " + "(A B \"C\") atom"); + assertArrayEquals(new String[]{"A", "B", "C"}, + r.readAtomStringList()); + assertEquals("atom", r.readAtomString()); + } + + /** + * Test empty astring lists. + */ + @Test + public void testAStringListEmpty() throws Exception { + Response r = new Response("* " + "()"); + assertArrayEquals(new String[0], r.readAtomStringList()); + } + + /** + * Test empty astring lists with more data following. + */ + @Test + public void testAStringListEmptyMore() throws Exception { + Response r = new Response("* " + "() atom"); + assertArrayEquals(new String[0], r.readAtomStringList()); + assertEquals("atom", r.readAtomString()); + } + + /** + * Test readStringList + */ + @Test + public void testBadStringList() throws Exception { + Response response = new Response( + "* (\"name\", \"test\", \"version\", \"1.0\")"); + String[] list = response.readStringList(); + // anything other than an infinite loop timeout is considered success + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPAlertTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPAlertTest.java new file mode 100644 index 0000000..617ac32 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPAlertTest.java @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.event.StoreEvent; +import jakarta.mail.event.StoreListener; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test alerts. + */ +public final class IMAPAlertTest { + + private volatile boolean gotAlert = false; + + @Test + public void test() { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandlerAlert(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + final CountDownLatch latch = new CountDownLatch(1); + + final Store store = session.getStore("imap"); + store.addStoreListener(new StoreListener() { + @Override + public void notification(StoreEvent e) { + String s; + if (e.getMessageType() == StoreEvent.ALERT) { + s = "ALERT: "; + gotAlert = true; + latch.countDown(); + } else + s = "NOTICE: "; + //System.out.println(s + e.getMessage()); + } + }); + try { + store.connect("test", "test"); + // time for event to be delivered + latch.await(5, TimeUnit.SECONDS); + assertTrue(gotAlert); + + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Returns an alert message at login. + */ + private static final class IMAPHandlerAlert extends IMAPHandler { + @Override + public void login() throws IOException { + untagged("OK [ALERT] account is over quota"); + super.login(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPAuthDebugTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPAuthDebugTest.java new file mode 100644 index 0000000..36f2e7d --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPAuthDebugTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +/** + * Test that authentication information is only included in + * the debug output when explicitly requested by setting the + * property "mail.debug.auth" to "true". + * + * XXX - should test all authentication types, but that requires + * more work in the dummy test server. + */ +@Timeout(20) +public final class IMAPAuthDebugTest { + + /** + * Test that authentication information isn't included in the debug output. + */ + @Test + public void testNoAuthDefault() { + final Properties properties = new Properties(); + assertFalse(test(properties, "LOGIN")); + } + + @Test + public void testNoAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "false"); + assertFalse(test(properties, "LOGIN")); + } + + /** + * Test that authentication information *is* included in the debug output. + */ + @Test + public void testAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "true"); + assertTrue(test(properties, "LOGIN")); + } + + /** + * Create a test server, connect to it, and collect the debug output. + * Scan the debug output looking for "expect", return true if found. + */ + public boolean test(Properties properties, String expect) { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandler(); + server = new TestServer(handler); + server.start(); + + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(bos); + session.setDebugOut(ps); + session.setDebug(true); + + final Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + + ps.close(); + bos.close(); + ByteArrayInputStream bis = + new ByteArrayInputStream(bos.toByteArray()); + BufferedReader r = new BufferedReader( + new InputStreamReader(bis, StandardCharsets.US_ASCII)); + String line; + boolean found = false; + while ((line = r.readLine()) != null) { + if (line.startsWith("DEBUG")) + continue; + if (line.startsWith("*")) + continue; + if (line.contains(expect)) + found = true; + } + r.close(); + return found; + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + return false; // XXX - doesn't matter + } finally { + if (server != null) { + server.quit(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPCloseFailureTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPCloseFailureTest.java new file mode 100644 index 0000000..7fefacf --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPCloseFailureTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Folder; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that failures while closing a folder are handled properly. + */ +public final class IMAPCloseFailureTest { + + private static final String HOST = "localhost"; + + static class NoIMAPHandler extends IMAPHandler { + static boolean first = true; + + @Override + public void examine(String line) throws IOException { + if (first) + no("mailbox gone"); + else + super.examine(line); + first = false; + } + } + + static class BadIMAPHandler extends IMAPHandler { + static boolean first = true; + + @Override + public void examine(String line) throws IOException { + if (first) + bad("mailbox gone"); + else + super.examine(line); + first = false; + } + } + + @Test + public void testCloseNo() { + testClose(new NoIMAPHandler()); + } + + @Test + public void testCloseBad() { + testClose(new BadIMAPHandler()); + } + + public void testClose(IMAPHandler handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.imap.host", HOST); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + Folder f = store.getFolder("INBOX"); + f.open(Folder.READ_WRITE); + f.close(false); + // Make sure that failure while closing doesn't leave us + // with a connection that can't be used to open a folder. + f.open(Folder.READ_WRITE); + f.close(false); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (store.isConnected()) + store.close(); + } + + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPConnectFailureTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPConnectFailureTest.java new file mode 100644 index 0000000..112d1f9 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPConnectFailureTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.iap.ConnectionException; +import org.xbib.net.mail.test.test.TestServer; +import org.xbib.net.mail.util.MailConnectException; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test connect failure. + */ +public final class IMAPConnectFailureTest { + + private static final String HOST = "localhost"; + private static final int CTO = 20; + + @Test + public void testNoServer() { + try { + // verify that port is not being used + ServerSocket ss = new ServerSocket(0); + int port = ss.getLocalPort(); + ss.close(); + + Properties properties = new Properties(); + properties.setProperty("mail.imap.host", HOST); + properties.setProperty("mail.imap.port", String.valueOf(port)); + properties.setProperty("mail.imap.connectiontimeout", "" + CTO); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + fail("Connected!"); + // failure! + } catch (MailConnectException mcex) { + // success! + assertEquals(HOST, mcex.getHost()); + assertEquals(port, mcex.getPort()); + assertEquals(CTO, mcex.getConnectionTimeout()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (store.isConnected()) + store.close(); + } + + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + /** + * Test that a disconnect after issuing the CAPABILITY command + * results in a ConnectionException. + */ + @Test + public void testCapabilityDisconnect() { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandler() { + @Override + public void sendGreetings() throws IOException { + untagged("OK IMAPHandler"); + } + + @Override + public void capability() throws IOException { + exit(); + } + }; + server = new TestServer(handler); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + fail("connect did not fail"); + } catch (MessagingException mex) { + // this is what we expect, now check that it was caused by + // the right exception + assertTrue(mex.getCause() instanceof ConnectionException); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPFetchProfileTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPFetchProfileTest.java new file mode 100644 index 0000000..67e5066 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPFetchProfileTest.java @@ -0,0 +1,198 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.FetchProfile; +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.imap.IMAPFolder; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.HashSet; +import java.util.Properties; +import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +/** + * Test IMAP FetchProfile items. + */ +@Timeout(20) +public final class IMAPFetchProfileTest { + + private static final String RDATE = "23-Jun-2004 06:26:26 -0700"; + private static final String ENVELOPE = + "(\"Wed, 23 Jun 2004 18:56:42 +0530\" \"test\" " + + "((\"Jakarta Mail\" NIL \"testuser\" \"example.com\")) " + + "((\"Jakarta Mail\" NIL \"testuser\" \"example.com\")) " + + "((\"Jakarta Mail\" NIL \"testuser\" \"example.com\")) " + + "((NIL NIL \"testuser\" \"example.com\")) NIL NIL NIL " + + "\"<40D98512.9040803@example.com>\")"; + + public static interface IMAPTest { + public void test(Folder folder, IMAPHandlerFetch handler) + throws MessagingException; + } + + @Test + public void testINTERNALDATEFetch() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerFetch handler) + throws MessagingException { + FetchProfile fp = new FetchProfile(); + fp.add(IMAPFolder.FetchProfileItem.INTERNALDATE); + Message m = folder.getMessage(1); + folder.fetch(new Message[]{m}, fp); + assertTrue(handler.saw("INTERNALDATE")); + handler.reset(); + assertTrue(m.getReceivedDate() != null); + assertFalse(handler.saw("INTERNALDATE")); + } + }, + new IMAPHandlerFetch() { + @Override + public void fetch(String line) throws IOException { + if (line.contains("INTERNALDATE")) + saw.add("INTERNALDATE"); + untagged("1 FETCH (INTERNALDATE \"" + RDATE + "\")"); + ok(); + } + }); + } + + @Test + public void testINTERNALDATEFetchEnvelope() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerFetch handler) + throws MessagingException { + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + Message m = folder.getMessage(1); + folder.fetch(new Message[]{m}, fp); + assertTrue(handler.saw("INTERNALDATE")); + handler.reset(); + assertTrue(m.getReceivedDate() != null); + assertFalse(handler.saw("INTERNALDATE")); + } + }, + new IMAPHandlerFetch() { + @Override + public void fetch(String line) throws IOException { + if (line.contains("INTERNALDATE")) + saw.add("INTERNALDATE"); + untagged("1 FETCH (INTERNALDATE \"" + RDATE + "\")"); + ok(); + } + }); + } + + @Test + public void testINTERNALDATENoFetch() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerFetch handler) + throws MessagingException { + Message m = folder.getMessage(1); + assertTrue(m.getReceivedDate() != null); + assertTrue(handler.saw("INTERNALDATE")); + } + }, + new IMAPHandlerFetch() { + @Override + public void fetch(String line) throws IOException { + if (line.contains("INTERNALDATE")) + saw.add("INTERNALDATE"); + untagged("1 FETCH (ENVELOPE " + ENVELOPE + + " INTERNALDATE \"" + RDATE + "\" RFC822.SIZE 0)"); + ok(); + } + }); + } + + public void testWithHandler(IMAPTest test, IMAPHandlerFetch handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_WRITE); + test.test(folder, handler); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. + */ + private static class IMAPHandlerFetch extends IMAPHandler { + // must be static because handler is cloned for each connection + protected static Set saw = new HashSet<>(); + + @Override + public void select(String line) throws IOException { + numberOfMessages = 1; + super.select(line); + } + + public boolean saw(String item) { + return saw.contains(item); + } + + public void reset() { + saw.clear(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPFolderTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPFolderTest.java new file mode 100644 index 0000000..3ce9bbe --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPFolderTest.java @@ -0,0 +1,531 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Folder; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.UIDFolder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.imap.IMAPFolder; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Properties; +import java.util.StringTokenizer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +/** + * Test IMAPFolder methods. + */ +@Timeout(20) +public final class IMAPFolderTest { + + private static final String utf8Folder = "test\u03b1"; + private static final String utf7Folder = "test&A7E-"; + + public static abstract class IMAPTest { + public void init(Properties props) { + } + + ; + + public abstract void test(Store store, IMAPHandler handler) + throws Exception; + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-7 + * encoded name for the CREATE command. + */ + @Test + public void testUtf7FolderNameCreate() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertTrue(test.create(Folder.HOLDS_MESSAGES)); + } + }, + new IMAPHandler() { + @Override + public void create(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "CREATE" + String name = st.nextToken(); + if (name.equals(utf7Folder)) + ok(); + else + no("wrong name"); + } + + @Override + public void list(String line) throws IOException { + untagged("LIST (\\HasNoChildren) \"/\" " + utf7Folder); + ok(); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the CREATE command. + */ + @Test + public void testUtf8FolderNameCreate() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertTrue(test.create(Folder.HOLDS_MESSAGES)); + } + }, + new IMAPUtf8Handler() { + @Override + public void create(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "CREATE" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) + ok(); + else + no("wrong name"); + } + + @Override + public void list(String line) throws IOException { + untagged("LIST (\\HasNoChildren) \"/\" \"" + + utf8Folder + "\""); + ok(); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-7 + * encoded name for the DELETE command. + */ + @Test + public void testUtf7FolderNameDelete() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertTrue(test.delete(false)); + } + }, + new IMAPHandler() { + @Override + public void delete(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "DELETE" + String name = st.nextToken(); + if (name.equals(utf7Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the DELETE command. + */ + @Test + public void testUtf8FolderNameDelete() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertTrue(test.delete(false)); + } + }, + new IMAPUtf8Handler() { + @Override + public void delete(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "DELETE" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-7 + * encoded name for the SELECT command. + */ + @Test + public void testUtf7FolderNameSelect() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + test.open(Folder.READ_WRITE); + test.close(true); + // no exception means success + } + }, + new IMAPHandler() { + @Override + public void select(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "SELECT" + String name = st.nextToken(); + if (name.equals(utf7Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the SELECT command. + */ + @Test + public void testUtf8FolderNameSelect() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + test.open(Folder.READ_WRITE); + test.close(true); + // no exception means success + } + }, + new IMAPUtf8Handler() { + @Override + public void select(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "SELECT" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-7 + * encoded name for the EXAMINE command. + */ + @Test + public void testUtf7FolderNameExamine() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + test.open(Folder.READ_ONLY); + test.close(true); + // no exception means success + } + }, + new IMAPHandler() { + @Override + public void examine(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "EXAMINE" + String name = st.nextToken(); + if (name.equals(utf7Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the EXAMINE command. + */ + @Test + public void testUtf8FolderNameExamine() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + test.open(Folder.READ_ONLY); + test.close(true); + // no exception means success + } + }, + new IMAPUtf8Handler() { + @Override + public void examine(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "EXAMINE" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) + ok(); + else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-7 + * encoded name for the STATUS command. + */ + @Test + public void testUtf7FolderNameStatus() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertEquals(123, ((UIDFolder) test).getUIDValidity()); + } + }, + new IMAPHandler() { + @Override + public void status(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "STATUS" + String name = st.nextToken(); + if (name.equals(utf7Folder)) { + untagged("STATUS " + utf7Folder + + " (UIDVALIDITY 123)"); + ok(); + } else + no("wrong name"); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the STATUS command. + */ + @Test + public void testUtf8FolderNameStatus() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder(utf8Folder); + assertEquals(123, ((UIDFolder) test).getUIDValidity()); + } + }, + new IMAPUtf8Handler() { + @Override + public void status(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "STATUS" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) { + untagged("STATUS \"" + utf8Folder + + "\" (UIDVALIDITY 123)"); + ok(); + } else + no("wrong name"); + } + }); + } + + /** + * Test that UIDNOTSTICKY is false in the formal case. + */ + @Test + public void testUidNotStickyFalse() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder("test"); + try { + test.open(Folder.READ_WRITE); + assertFalse(((IMAPFolder) test).getUIDNotSticky()); + } finally { + test.close(); + } + } + }, + new IMAPHandler()); + } + + /** + * Test that UIDNOTSTICKY is true when the untagged response is included. + */ + @Test + public void testUidNotStickyTrue() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder("test"); + try { + test.open(Folder.READ_WRITE); + assertTrue(((IMAPFolder) test).getUIDNotSticky()); + } finally { + test.close(); + } + } + }, + new IMAPHandler() { + @Override + public void select(String line) throws IOException { + untagged("NO [UIDNOTSTICKY]"); + super.select(line); + } + }); + } + + /** + * Test that EXPUNGE responses with out-of-range message numbers + * are ignored. + */ + @Test + public void testExpungeOutOfRange() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, IMAPHandler handler) + throws MessagingException, IOException { + Folder test = store.getFolder("test"); + try { + test.open(Folder.READ_WRITE); + // no way to force a noop without waiting so do this + assertEquals(0, test.getUnreadMessageCount()); + assertEquals(0, test.getMessageCount()); + } finally { + test.close(); + } + } + }, + new IMAPHandler() { + @Override + public void search(String line) throws IOException { + untagged("1 EXPUNGE"); + untagged("0 EXISTS"); + super.search(line); + } + }); + } + + private void testWithHandler(IMAPTest test, IMAPHandler handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + test.init(properties); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + test.test(store, handler); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + private static String unquote(String s) { + if (s.startsWith("\"") && s.endsWith("\"") && s.length() > 1) { + s = s.substring(1, s.length() - 1); + // check for any escaped characters + if (s.indexOf('\\') >= 0) { + StringBuilder sb = new StringBuilder(s.length()); // approx + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && i < s.length() - 1) + c = s.charAt(++i); + sb.append(c); + } + s = sb.toString(); + } + } + return s; + } + + /** + * An IMAPHandler that enables UTF-8 support. + */ + private static class IMAPUtf8Handler extends IMAPHandler { + { + { + capabilities += " ENABLE UTF8=ACCEPT"; + } + } + + @Override + public void enable(String line) throws IOException { + ok(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPHandler.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPHandler.java new file mode 100644 index 0000000..fc751c6 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPHandler.java @@ -0,0 +1,594 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import org.xbib.net.mail.test.test.ProtocolHandler; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.StringTokenizer; +import java.util.logging.Level; + +/** + * Handle IMAP connection. + * + * @author Bill Shannon + */ +public class IMAPHandler extends ProtocolHandler { + + /** + * Current line. + */ + private String currentLine; + + /** + * Tag for current command + */ + protected String tag; + + /** + * IMAP capabilities supported + */ + protected String capabilities = "IMAP4REV1 IDLE ID"; + + /** + * Number of messages + */ + protected int numberOfMessages = 0; + + /** + * Number of recent messages + */ + protected int numberOfRecentMessages = 0; + + /** + * Send greetings. + * + * @throws IOException unable to write to socket + */ + @Override + public void sendGreetings() throws IOException { + untagged("OK [CAPABILITY " + capabilities + "] IMAPHandler"); + } + + /** + * Send String to socket. + * + * @param str String to send + * @throws IOException unable to write to socket + */ + public void println(final String str) throws IOException { + writer.print(str); + writer.print("\r\n"); + writer.flush(); + } + + /** + * Send a tagged response. + * + * @param resp the response to send + * @throws IOException unable to read/write to socket + */ + public void tagged(final String resp) throws IOException { + println(tag + " " + resp); + } + + /** + * Send an untagged response. + * + * @param resp the response to send + * @throws IOException unable to read/write to socket + */ + public void untagged(final String resp) throws IOException { + println("* " + resp); + } + + /** + * Send a tagged OK response. + * + * @throws IOException unable to read/write to socket + */ + public void ok() throws IOException { + tagged("OK"); + } + + /** + * Send a tagged OK response with a message. + * + * @param msg the message to send + * @throws IOException unable to read/write to socket + */ + public void ok(final String msg) throws IOException { + tagged("OK " + (msg != null ? msg : "")); + } + + /** + * Send a tagged NO response with a message. + * + * @param msg the message to send + * @throws IOException unable to read/write to socket + */ + public void no(final String msg) throws IOException { + tagged("NO " + (msg != null ? msg : "")); + } + + /** + * Send a tagged BAD response with a message. + * + * @param msg the message to send + * @throws IOException unable to read/write to socket + */ + public void bad(final String msg) throws IOException { + tagged("BAD " + (msg != null ? msg : "")); + } + + /** + * Send an untagged BYE response with a message, then exit. + * + * @param msg the message to send + * @throws IOException unable to read/write to socket + */ + public void bye(final String msg) throws IOException { + untagged("BYE " + (msg != null ? msg : "")); + exit(); + } + + /** + * Send a "continue" command. + * + * @throws IOException unable to read/write to socket + */ + public void cont() throws IOException { + println("+ please continue"); + } + + /** + * Send a "continue" command with a message. + * + * @throws IOException unable to read/write to socket + */ + public void cont(String msg) throws IOException { + println("+ " + (msg != null ? msg : "")); + } + + /** + * Handle command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void handleCommand() throws IOException { + currentLine = super.readLine(); + + if (currentLine == null) { + // probably just EOF because the socket was closed + //LOGGER.severe("Current line is null!"); + exit(); + return; + } + + StringTokenizer ct = new StringTokenizer(currentLine, " "); + if (!ct.hasMoreTokens()) { + LOGGER.log(Level.SEVERE, "ERROR no command tag: {0}", + escape(currentLine)); + bad("no command tag"); + return; + } + tag = ct.nextToken(); + if (!ct.hasMoreTokens()) { + LOGGER.log(Level.SEVERE, "ERROR no command: {0}", + escape(currentLine)); + bad("no command"); + return; + } + final String commandName = ct.nextToken().toUpperCase(); + if (commandName == null) { + LOGGER.severe("Command name is empty!"); + exit(); + return; + } + + if (commandName.equals("LOGIN")) { + login(); + } else if (commandName.equals("AUTHENTICATE")) { + String mech = ct.nextToken().toUpperCase(); + String ir = null; + if (ct.hasMoreTokens()) + ir = ct.nextToken(); + authenticate(mech, ir); + } else if (commandName.equals("CAPABILITY")) { + capability(); + } else if (commandName.equals("NOOP")) { + noop(); + } else if (commandName.equals("SELECT")) { + select(currentLine); + } else if (commandName.equals("EXAMINE")) { + examine(currentLine); + } else if (commandName.equals("LIST")) { + list(currentLine); + } else if (commandName.equals("IDLE")) { + idle(); + } else if (commandName.equals("FETCH")) { + fetch(currentLine); + } else if (commandName.equals("STORE")) { + store(currentLine); + } else if (commandName.equals("SEARCH")) { + search(currentLine); + } else if (commandName.equals("APPEND")) { + append(currentLine); + } else if (commandName.equals("CLOSE")) { + close(); + } else if (commandName.equals("LOGOUT")) { + logout(); + } else if (commandName.equals("UID")) { + String subcommandName = ct.nextToken().toUpperCase(); + if (subcommandName.equals("FETCH")) { + uidfetch(currentLine); + } else if (subcommandName.equals("STORE")) { + uidstore(currentLine); + } else { + LOGGER.log(Level.SEVERE, "ERROR UID command unknown: {0}", + subcommandName); + bad("unknown UID command"); + } + } else if (commandName.equals("ID")) { + id(currentLine); + } else if (commandName.equals("ENABLE")) { + enable(currentLine); + } else if (commandName.equals("CREATE")) { + create(currentLine); + } else if (commandName.equals("DELETE")) { + delete(currentLine); + } else if (commandName.equals("STATUS")) { + status(currentLine); + } else if (commandName.equals("NAMESPACE")) { + namespace(); + } else { + LOGGER.log(Level.SEVERE, "ERROR command unknown: {0}", + escape(currentLine)); + bad("unknown command"); + } + } + + /** + * LOGIN command. + * + * @throws IOException unable to read/write to socket + */ + public void login() throws IOException { + ok("[CAPABILITY " + capabilities + "]"); + } + + /** + * AUTHENTICATE command. + * + * @throws IOException unable to read/write to socket + */ + public void authenticate(String mech, String ir) throws IOException { + if (mech.equals("LOGIN")) + authlogin(ir); + else if (mech.equals("PLAIN")) + authplain(ir); + else + bad("AUTHENTICATE not supported"); + } + + /** + * AUTHENTICATE LOGIN command. + * + * @throws IOException unable to read/write to socket + */ + public void authlogin(String ir) throws IOException { + if (ir != null) + bad("AUTHENTICATE LOGIN does not support initial response"); + cont(base64encode("Username")); + String username = readLine(); + cont(base64encode("Password")); + String password = readLine(); + ok("[CAPABILITY " + capabilities + "]"); + } + + /** + * AUTHENTICATE PLAIN command. + * + * @throws IOException unable to read/write to socket + */ + public void authplain(String ir) throws IOException { + if (ir == null) { + cont(""); + String resp = readLine(); + } + ok("[CAPABILITY " + capabilities + "]"); + } + + /** + * CAPABILITY command. + * + * @throws IOException unable to read/write to socket + */ + public void capability() throws IOException { + untagged("CAPABILITY " + capabilities); + ok(); + } + + /** + * SELECT command. + * + * @throws IOException unable to read/write to socket + */ + public void select(String line) throws IOException { + untagged(numberOfMessages + " EXISTS"); + untagged(numberOfRecentMessages + " RECENT"); + ok(); + } + + /** + * EXAMINE command. + * + * @throws IOException unable to read/write to socket + */ + public void examine(String line) throws IOException { + untagged(numberOfMessages + " EXISTS"); + untagged(numberOfRecentMessages + " RECENT"); + ok(); + } + + /** + * LIST command. + * + * @throws IOException unable to read/write to socket + */ + public void list(String line) throws IOException { + ok(); + } + + /** + * IDLE command. + * + * @throws IOException unable to read/write to socket + */ + public void idle() throws IOException { + cont(); + idleWait(); + ok(); + } + + @Override + protected String readLine() throws IOException { + currentLine = super.readLine(); + if (currentLine == null) { + LOGGER.severe("Current line is null!"); + exit(); + } + return currentLine; + } + + protected void idleWait() throws IOException { + String line = readLine(); + + if (line != null && !line.equalsIgnoreCase("DONE")) { + LOGGER.severe("Didn't get DONE response to IDLE"); + exit(); + return; + } + } + + /** + * FETCH command. + * + * @throws IOException unable to read/write to socket + */ + public void fetch(String line) throws IOException { + ok(); // XXX + } + + /** + * STORE command. + * + * @throws IOException unable to read/write to socket + */ + public void store(String line) throws IOException { + ok(); // XXX + } + + /** + * SEARCH command. + * + * @throws IOException unable to read/write to socket + */ + public void search(String line) throws IOException { + untagged("SEARCH"); + ok(); // XXX + } + + /** + * UID FETCH command. + * + * @throws IOException unable to read/write to socket + */ + public void uidfetch(String line) throws IOException { + ok(); // XXX + } + + /** + * UID STORE command. + * + * @throws IOException unable to read/write to socket + */ + public void uidstore(String line) throws IOException { + ok(); // XXX + } + + /** + * APPEND command. + * + * @throws IOException unable to read/write to socket + */ + public void append(String line) throws IOException { + int left = line.lastIndexOf('{'); + int right = line.indexOf('}', left); + int bytes = Integer.parseInt(line.substring(left + 1, right)); + cont("waiting for message"); + collectMessage(bytes); + ok(); // XXX + } + + /** + * ID command. + * + * @throws IOException unable to read/write to socket + */ + public void id(String line) throws IOException { + untagged("ID NIL"); + ok(); + } + + /** + * ENABLE command. + * + * @throws IOException unable to read/write to socket + */ + public void enable(String line) throws IOException { + no("can't enable"); + } + + /** + * CREATE command. + * + * @throws IOException unable to read/write to socket + */ + public void create(String line) throws IOException { + no("can't create"); + } + + /** + * DELETE command. + * + * @throws IOException unable to read/write to socket + */ + public void delete(String line) throws IOException { + no("can't delete"); + } + + /** + * STATUS command. + * + * @throws IOException unable to read/write to socket + */ + public void status(String line) throws IOException { + no("can't get status"); + } + + /** + * NAMESPACE command. + * + * @throws IOException unable to read/write to socket + */ + public void namespace() throws IOException { + no("no namespaces"); + } + + /** + * Collect "bytes" worth of data for the message being appended. + */ + protected void collectMessage(int bytes) throws IOException { + readLiteral(bytes); // read the data and throw it away + super.readLine(); // data followed by a newline + } + + /** + * Read a literal of "bytes" bytes and return it as a UTF-8 string. + */ + protected String readLiteral(int bytes) throws IOException { + println("+"); + byte[] data = new byte[bytes]; + int len = data.length; + int off = 0; + int n; + while (len > 0 && (n = in.read(data, off, len)) > 0) { + off += n; + len -= n; + } + return new String(data, StandardCharsets.UTF_8); + } + + /** + * CLOSE command. + * + * @throws IOException unable to read/write to socket + */ + public void close() throws IOException { + ok(); + } + + /** + * NOOP command. + * + * @throws IOException unable to read/write to socket + */ + public void noop() throws IOException { + ok(); + } + + /** + * LOGOUT command. + * + * @throws IOException unable to read/write to socket + */ + public void logout() throws IOException { + ok(); + exit(); + } + + /** + * Base64 encode the string. + */ + protected String base64encode(String s) throws IOException { + return new String(Base64.getEncoder().encode(s.getBytes(StandardCharsets.US_ASCII)), + StandardCharsets.US_ASCII); + } + + /** + * Escape any non-printable characters in "s", + * limiting total length to about 100 characters. + */ + private String escape(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + if (sb.length() >= 100) { + sb.append("..."); + break; + } + char c = s.charAt(i); + if (c < ' ' || c == '\177') { + if (c == '\r') + sb.append("\\r"); + else if (c == '\n') + sb.append("\\n"); + else if (c == '\t') + sb.append("\\t"); + else + sb.append('\\').append(String.format("%03o", (int) c)); + } else if (c >= '\200') { + sb.append("\\u").append(String.format("%04x", (int) c)); + } else + sb.append(c); + } + return sb.toString(); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIDTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIDTest.java new file mode 100644 index 0000000..3dd5de4 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIDTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Session; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.imap.IMAPStore; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Map; +import java.util.Properties; +import java.util.StringTokenizer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + + +/** + * Test the IMAP ID command. + */ +@Timeout(20) +public final class IMAPIDTest { + + @Test + public void testIDNIL() { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandlerID(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final IMAPStore store = (IMAPStore) session.getStore("imap"); + try { + store.connect("test", "test"); + Map id = store.id(null); + assertEquals("true", id.get("test")); + + } catch (Exception ex) { + System.out.println(ex); + ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. + */ + private static final class IMAPHandlerID extends IMAPHandler { + + @Override + public void id(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + String tag = st.nextToken(); + String cmd = st.nextToken(); + String arg = st.nextToken(); + untagged("ID (\"test\" \"" + arg.equals("NIL") + "\")"); + ok(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleManagerTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleManagerTest.java new file mode 100644 index 0000000..b2f29ad --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleManagerTest.java @@ -0,0 +1,539 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.FetchProfile; +import jakarta.mail.Folder; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.imap.IMAPStore; +import org.xbib.net.mail.imap.IdleManager; +import org.xbib.net.mail.test.test.TestServer; +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test IdleManager. + */ +@Timeout(10) +public final class IMAPIdleManagerTest { + + /** + * Test that IdleManager handles multiple responses in a single packet. + */ + @Test + public void testDone() { + testSuccess(false, new IMAPHandlerIdleDone(), false); + } + + @Test + public void testExists() { + testSuccess(false, new IMAPHandlerIdleExists(), false); + } + + @Test + public void testDoneProtocolFindSocketChannel() { + testSuccess(true, new IMAPHandlerIdleDone(), false); + } + + @Test + public void testExistsProtocolFindSocketChannel() { + testSuccess(true, new IMAPHandlerIdleExists(), false); + } + + private void testSuccess(boolean isSSL, IMAPHandlerIdle handler, boolean setTimeout) { + TestServer server = null; + IdleManager idleManager = null; + ExecutorService executor = Executors.newCachedThreadPool(); + try { + server = new TestServer(handler, isSSL); + server.start(); + + final Properties properties = new Properties(); + if (isSSL) { + properties.setProperty("mail.imaps.host", "localhost"); + properties.setProperty("mail.imaps.port", String.valueOf(server.getPort())); + properties.setProperty("mail.imaps.socketFactory.class", + "org.xbib.net.mail.util.MailSSLSocketFactory"); + properties.setProperty("mail.imaps.ssl.trust", "*"); + properties.setProperty("mail.imaps.ssl.checkserveridentity", "false"); + properties.setProperty("mail.imaps.usesocketchannels", "true"); + if (setTimeout) + properties.setProperty("mail.imaps.timeout", "" + 1000); + } else { + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + properties.setProperty("mail.imap.usesocketchannels", "true"); + if (setTimeout) + properties.setProperty("mail.imap.timeout", "" + 1000); + } + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + idleManager = new IdleManager(session, executor); + + final IMAPStore store = (IMAPStore) session + .getStore(isSSL ? "imaps" : "imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_WRITE); + idleManager.watch(folder); + handler.waitForIdle(); + + // now do something that is sure to touch the server + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + folder.fetch(folder.getMessages(), fp); + + // check that the new message was seen + int count = folder.getMessageCount(); + folder.close(true); + + assertEquals(3, count); + } catch (Exception ex) { + System.out.println(ex); + System.out.flush(); + ex.printStackTrace(); + System.err.flush(); + fail(ex.toString()); + } finally { + try { + folder.close(false); + } catch (Exception ex2) { + } + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + System.err.flush(); + fail(e.getMessage()); + } finally { + executor.shutdown(); + if (idleManager != null) + idleManager.stop(); + if (server != null) { + server.quit(); + } + } + } + + /** + * Test that IdleManager handles timeouts. + */ + @Test + public void testBeforeIdleTimeout() { + testFailure(new IMAPHandlerBeforeIdleTimeout(), true); + } + + @Test + public void testIdleTimeout() { + testFailure(new IMAPHandlerIdleTimeout(), true); + } + + @Test + public void testDoneTimeout() { + testFailure(new IMAPHandlerDoneTimeout(), true); + } + + /** + * Test that IdleManager handles connection failures. + */ + @Test + public void testBeforeIdleDrop() { + testFailure(new IMAPHandlerBeforeIdleDrop(), false); + } + + @Test + public void testIdleDrop() { + testFailure(new IMAPHandlerIdleDrop(), false); + } + + @Test + public void testDoneDrop() { + testFailure(new IMAPHandlerDoneDrop(), false); + } + + private void testFailure(IMAPHandlerIdle handler, boolean setTimeout) { + TestServer server = null; + IdleManager idleManager = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + if (setTimeout) + properties.setProperty("mail.imap.timeout", "" + 1000); + properties.setProperty("mail.imap.usesocketchannels", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + ExecutorService executor = Executors.newCachedThreadPool(); + idleManager = new IdleManager(session, executor); + + final IMAPStore store = (IMAPStore) session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_WRITE); + idleManager.watch(folder); + handler.waitForIdle(); + + // now do something that is sure to touch the server + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + folder.fetch(folder.getMessages(), fp); + + fail("No exception"); + } catch (MessagingException mex) { + // success! + } catch (Exception ex) { + System.out.println("Failed with exception: " + ex); + ex.printStackTrace(); + fail(ex.toString()); + } finally { + try { + folder.close(false); + } catch (Exception ex2) { + } + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (idleManager != null) + idleManager.stop(); + if (server != null) { + server.quit(); + } + } + } + + @Test + public void testNotOpened() { + TestServer server = null; + IdleManager idleManager = null; + try { + server = new TestServer(new IMAPHandler()); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + properties.setProperty("mail.imap.usesocketchannels", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + ExecutorService executor = Executors.newCachedThreadPool(); + idleManager = new IdleManager(session, executor); + + final IMAPStore store = (IMAPStore) session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + idleManager.watch(folder); + + fail("No exception"); + } catch (MessagingException mex) { + // make sure we get the expected exception + assertTrue(mex.getMessage().contains("open")); + // success! + } catch (Exception ex) { + System.out.println("Failed with exception: " + ex); + ex.printStackTrace(); + fail(ex.toString()); + } finally { + try { + folder.close(false); + } catch (Exception ex2) { + } + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (idleManager != null) + idleManager.stop(); + if (server != null) { + server.quit(); + } + } + } + + @Test + public void testNoSocketChannel() { + TestServer server = null; + IdleManager idleManager = null; + try { + server = new TestServer(new IMAPHandler()); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + ExecutorService executor = Executors.newCachedThreadPool(); + idleManager = new IdleManager(session, executor); + + final IMAPStore store = (IMAPStore) session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_WRITE); + idleManager.watch(folder); + + fail("No exception"); + } catch (MessagingException mex) { + // make sure we get the expected exception + assertTrue(!mex.getMessage().contains("open")); + // success! + } catch (Exception ex) { + System.out.println("Failed with exception: " + ex); + ex.printStackTrace(); + fail(ex.toString()); + } finally { + try { + folder.close(false); + } catch (Exception ex2) { + } + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (idleManager != null) + idleManager.stop(); + if (server != null) { + server.quit(); + } + } + } + + /** + * Base class for custom handler. + */ + private static abstract class IMAPHandlerIdle extends IMAPHandler { + @Override + public void select(String line) throws IOException { + numberOfMessages = 1; + super.select(line); + } + + public abstract void waitForIdle() throws InterruptedException; + } + + /** + * Custom handler. Respond to DONE with a single packet containing + * EXISTS and OK. + */ + private static final class IMAPHandlerIdleDone extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + idleWait(); + println("* 3 EXISTS\r\n" + tag + " OK"); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Send two EXISTS responses in a single packet. + */ + private static final class IMAPHandlerIdleExists extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + idleWait(); + println("* 2 EXISTS\r\n* 3 EXISTS"); + ok(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Delay long enough before IDLE starts to force a timeout. + */ + private static final class IMAPHandlerBeforeIdleTimeout + extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + try { + Thread.sleep(2 * 1000); + } catch (InterruptedException ex) { + } + cont(); + latch.countDown(); + idleWait(); + ok(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Delay long enough after IDLE starts to force a timeout. + */ + private static final class IMAPHandlerIdleTimeout extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + try { + Thread.sleep(2 * 1000); + } catch (InterruptedException ex) { + } + idleWait(); + ok(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Delay long enough after DONE received to force a + * timeout. + */ + private static final class IMAPHandlerDoneTimeout extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + idleWait(); + try { + Thread.sleep(2 * 1000); + } catch (InterruptedException ex) { + } + ok(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Drop the connection before IDLE started. + */ + private static final class IMAPHandlerBeforeIdleDrop extends + IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + latch.countDown(); + exit(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Drop the connection after IDLE started. + */ + private static final class IMAPHandlerIdleDrop extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + exit(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } + + /** + * Custom handler. Drop the connection after DONE received. + */ + private static final class IMAPHandlerDoneDrop extends IMAPHandlerIdle { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + idleWait(); + exit(); + } + + @Override + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleStateTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleStateTest.java new file mode 100644 index 0000000..4e71a74 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleStateTest.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Session; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.imap.IMAPStore; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that IMAP idle state is handled properly. + */ +@Timeout(20) +public final class IMAPIdleStateTest { + + @Test + public void testInsecure() { + test(false); + } + + @Test + public void testProtocolFindSocketChannel() { + test(true); + } + + private void test(boolean isSSL) { + TestServer server = null; + try { + final IMAPHandlerIdleBye handler = new IMAPHandlerIdleBye(); + server = new TestServer(handler, isSSL); + server.start(); + + final Properties properties = new Properties(); + if (isSSL) { + properties.setProperty("mail.imaps.host", "localhost"); + properties.setProperty("mail.imaps.port", String.valueOf(server.getPort())); + properties.setProperty("mail.imaps.socketFactory.class", + "org.xbib.net.mail.util.MailSSLSocketFactory"); + properties.setProperty("mail.imaps.ssl.trust", "*"); + properties.setProperty("mail.imaps.ssl.checkserveridentity", "false"); + //mail.imaps.usesocketchannels is not set which forces default of false. + } else { + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + } + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final IMAPStore store = (IMAPStore) session.getStore( + isSSL ? "imaps" : "imap"); + try { + store.connect("test", "test"); + + // create a thread to run the IDLE command on the Store + Thread t = new Thread() { + @Override + public void run() { + try { + store.idle(); + } catch (Exception ex) { + } + } + }; + t.start(); + handler.waitForIdle(); + + // Now break it out of idle. + // Need to use a method that doesn't check that the Store + // is connected first. + store.hasCapability("XXX"); + // no NullPointerException means the bug is fixed! + + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Simulates the server sending a BYE response + * to abort an IDLE. + */ + private static final class IMAPHandlerIdleBye extends IMAPHandler { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void idle() throws IOException { + cont(); + latch.countDown(); + // don't wait for DONE, just close the connection now + bye("closing"); + } + + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleUntaggedResponseTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleUntaggedResponseTest.java new file mode 100644 index 0000000..77ce6cc --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPIdleUntaggedResponseTest.java @@ -0,0 +1,154 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.FetchProfile; +import jakarta.mail.Folder; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.imap.IMAPFolder; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test untagged responses before IDLE continuation. + */ +@Timeout(20) +public final class IMAPIdleUntaggedResponseTest { + + @Test + public void test() { + test(false); + } + + @Test + public void testProtocolFindSocketChannel() { + test(true); + } + + private void test(boolean isSSL) { + TestServer server = null; + try { + final IMAPHandlerIdleExists handler = new IMAPHandlerIdleExists(); + server = new TestServer(handler, isSSL); + server.start(); + + final Properties properties = new Properties(); + if (isSSL) { + properties.setProperty("mail.imaps.host", "localhost"); + properties.setProperty("mail.imaps.port", String.valueOf(server.getPort())); + properties.setProperty("mail.imaps.socketFactory.class", + "org.xbib.net.mail.util.MailSSLSocketFactory"); + properties.setProperty("mail.imaps.ssl.trust", "*"); + properties.setProperty("mail.imaps.ssl.checkserveridentity", "false"); + //Add property and set to false. + properties.setProperty("mail.imaps.usesocketchannels", "false"); + } else { + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + } + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore(isSSL ? "imaps" : "imap"); + Folder folder0 = null; + try { + store.connect("test", "test"); + final Folder folder = store.getFolder("INBOX"); + folder0 = folder; + folder.open(Folder.READ_ONLY); + + // create a thread to make sure we're kicked out of idle + Thread t = new Thread() { + @Override + public void run() { + try { + handler.waitForIdle(); + // now do something that is sure to touch the server + FetchProfile fp = new FetchProfile(); + fp.add(FetchProfile.Item.ENVELOPE); + folder.fetch(folder.getMessages(), fp); + } catch (Exception ex) { + //ex.printStackTrace(System.out); + } + } + }; + t.start(); + + ((IMAPFolder) folder).idle(); + + assertEquals(1, folder.getMessageCount()); + + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + System.out.flush(); + fail(ex.toString()); + } finally { + if (folder0 != null) + folder0.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + System.err.flush(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Returns untagged responses before continuation, + * followed by a flag change for one of the new messages, to make + * sure the notification of the new message is seen. + */ + private static final class IMAPHandlerIdleExists extends IMAPHandler { + // must be static because handler is cloned for each connection + private static CountDownLatch latch = new CountDownLatch(1); + + @Override + public void examine(String line) throws IOException { + numberOfMessages = 1; + super.examine(line); + } + + @Override + public void idle() throws IOException { + untagged("1 EXISTS"); + untagged("1 RECENT"); + cont(); + untagged("1 FETCH (FLAGS (\\Recent \\Seen))"); + latch.countDown(); + idleWait(); + ok(); + } + + public void waitForIdle() throws InterruptedException { + latch.await(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginCapabilitiesTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginCapabilitiesTest.java new file mode 100644 index 0000000..8201464 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginCapabilitiesTest.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Session; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.imap.IMAPStore; +import org.xbib.net.mail.test.test.TestServer; +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that capabilities are updated after login. + */ +@Timeout(5) +public final class IMAPLoginCapabilitiesTest { + + private static final String NEWCAP = "NEWCAP"; + + /** + * Test untagged CAPABILITY response after LOGIN. + * This is illegal, but mail.ru and AOL do it. + */ + @Test + public void testUntaggedCapabilityAfterLogin() { + test(new IMAPHandler() { + @Override + public void login() throws IOException { + untagged("CAPABILITY " + capabilities + + " " + NEWCAP); + ok("LOGIN completed"); + } + }); + } + + /** + * Test multiple untagged CAPABILITY responses after LOGIN. + * This should NEVER happen, but we handle it just in case. + */ + @Test + public void testMultipleUntaggedCapabilityAfterLogin() { + test(new IMAPHandler() { + @Override + public void login() throws IOException { + untagged("CAPABILITY " + capabilities); + untagged("CAPABILITY " + NEWCAP); + ok("LOGIN completed"); + } + }); + } + + /** + * Test untagged CAPABILITY response after AUTHENTICATE. + */ + @Test + public void testUntaggedCapabilityAfterAuthenticate() { + test(new IMAPHandler() { + { + { + capabilities += " AUTH=PLAIN"; + } + } + + @Override + public void authplain(String ir) throws IOException { + untagged("CAPABILITY " + capabilities + + " " + NEWCAP); + ok("AUTHENTICATE completed"); + } + }); + } + + private void test(IMAPHandler handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + //properties.setProperty("mail.debug.auth", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final IMAPStore store = (IMAPStore) session.getStore("imap"); + try { + store.connect("test", "test"); + assertTrue(store.hasCapability(NEWCAP)); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginFailureTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginFailureTest.java new file mode 100644 index 0000000..b3e737c --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginFailureTest.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.SavedSocketFactory; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that login failures are handled correctly. + */ +public final class IMAPLoginFailureTest { + + /** + * Test that login failures when no login methods are supported + * cause the socket to be closed. + */ + @Test + public void testSocketClosed() { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandler() { + @Override + public void sendGreetings() throws IOException { + capabilities = "IMAP4REV1 LOGINDISABLED"; + super.sendGreetings(); + } + }; + server = new TestServer(handler); + server.start(); + + SavedSocketFactory ssf = new SavedSocketFactory(); + Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + properties.put("mail.imap.socketFactory", ssf); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + fail("login did not fail"); + } catch (MessagingException mex) { + // this is what we expect, now check that the socket is closed + assertTrue(ssf.getSocket().isClosed()); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginHandler.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginHandler.java new file mode 100644 index 0000000..05c39e9 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginHandler.java @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Handle IMAP connection with LOGIN authentication. + * + * @author Bill Shannon + */ +public class IMAPLoginHandler extends IMAPHandler { + + protected String username = "test"; + protected String password = "test"; + + public IMAPLoginHandler() { + capabilities += " LOGINDISABLED AUTH=LOGIN"; + } + + /** + * AUTHENTICATE LOGIN command. + * + * @throws IOException unable to read/write to socket + */ + public void authlogin(String ir) throws IOException { + if (ir != null) + bad("AUTHENTICATE LOGIN does not support initial response"); + cont(base64encode("Username")); + String resp = readLine(); + String u = new String(Base64.getDecoder().decode( + resp.getBytes(StandardCharsets.US_ASCII)), + StandardCharsets.UTF_8); + cont(base64encode("Password")); + resp = readLine(); + String p = new String(Base64.getDecoder().decode( + resp.getBytes(StandardCharsets.US_ASCII)), + StandardCharsets.UTF_8); + //System.out.printf("USER: %s, PASSWORD: %s%n", u, p); + if (!u.equals(username) || !p.equals(password)) { + no("authentication failed"); + return; + } + ok("[CAPABILITY " + capabilities + "]"); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginReferralTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginReferralTest.java new file mode 100644 index 0000000..10b3f11 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPLoginReferralTest.java @@ -0,0 +1,173 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Session; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.imap.IMAPStore; +import org.xbib.net.mail.imap.ReferralException; +import org.xbib.net.mail.test.test.TestServer; +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test IMAP login referrals (RFC 2221). + */ +@Timeout(5) +public final class IMAPLoginReferralTest { + + private static final String REFERRAL_URL = "imap://test@server/"; + private static final String REFERRAL_MSG = "try server"; + private static final String REFERRAL = + "[REFERRAL " + REFERRAL_URL + "] " + REFERRAL_MSG; + + private static final int TIMEOUT = 1000; // 1 second + + /** + * Test referral in BYE when connecting. + */ + @Test + public void testConnectReferral() { + test(new IMAPHandler() { + @Override + public void sendGreetings() throws IOException { + bye(REFERRAL); + } + }); + } + + /** + * Test referral in NO response to LOGIN. + */ + @Test + public void testLoginReferral() { + test(new IMAPHandler() { + { + { + capabilities += " LOGIN-REFERRALS"; + } + } + + @Override + public void login() throws IOException { + no(REFERRAL); + } + }); + } + + /** + * Test referral in OK response to LOGIN. + */ + @Test + public void testLoginOkReferral() { + test(new IMAPHandler() { + { + { + capabilities += " LOGIN-REFERRALS"; + } + } + + @Override + public void login() throws IOException { + ok(REFERRAL); + } + }); + } + + /** + * Test referral in NO response to AUTHENTICATE PLAIN. + */ + @Test + public void testPlainReferral() { + test(new IMAPHandler() { + { + { + capabilities += " LOGIN-REFERRALS AUTH=PLAIN"; + } + } + + @Override + public void authplain(String ir) throws IOException { + no(REFERRAL); + } + }); + } + + /** + * Test referral in OK response to AUTHENTICATE PLAIN. + */ + @Test + public void testPlainOkReferral() { + test(new IMAPHandler() { + { + { + capabilities += " LOGIN-REFERRALS AUTH=PLAIN"; + } + } + + @Override + public void authplain(String ir) throws IOException { + if (ir == null) { + cont(""); + String resp = readLine(); + } + ok(REFERRAL); + } + }); + } + + private void test(IMAPHandler handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + properties.setProperty("mail.imap.referralexception", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final IMAPStore store = (IMAPStore) session.getStore("imap"); + try { + store.connect("test", "test"); + fail("connect succeeded"); + } catch (ReferralException ex) { + // success! + assertEquals(ex.getUrl(), REFERRAL_URL); + assertEquals(ex.getText(), REFERRAL_MSG); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPMessageNumberOutOfRangeTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPMessageNumberOutOfRangeTest.java new file mode 100644 index 0000000..452c4e4 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPMessageNumberOutOfRangeTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Flags; +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.search.FlagTerm; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.test.test.TestServer; +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test FETCH and SEARCH responses with a message number that's out of range. + */ +@Timeout(20) +public final class IMAPMessageNumberOutOfRangeTest { + + @Test + public void test() { + TestServer server = null; + try { + final IMAPHandlerBad handler = new IMAPHandlerBad(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + Message msg = folder.getMessage(1); + Flags f = msg.getFlags(); + Message[] msgs = folder.search( + new FlagTerm(new Flags(Flags.Flag.RECENT), true)); + assertEquals(1, msgs.length); + assertEquals(msg, msgs[0]); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Returns responses for messages that don't + * exist in the folder. + */ + private static final class IMAPHandlerBad extends IMAPHandler { + + @Override + public void examine(String line) throws IOException { + numberOfMessages = 1; + numberOfRecentMessages = 1; + super.examine(line); + } + + @Override + public void search(String line) throws IOException { + untagged("SEARCH 1 2"); + ok(); + } + + @Override + public void fetch(String line) throws IOException { + untagged("1 FETCH (FLAGS (\\Recent))"); + untagged("2 FETCH (FLAGS (\\Deleted))"); + ok(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPMessageTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPMessageTest.java new file mode 100644 index 0000000..132215e --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPMessageTest.java @@ -0,0 +1,407 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.BodyPart; +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.internet.MimeUtility; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.imap.IMAPMessage; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test IMAPMessage methods. + */ +@Timeout(20) +public final class IMAPMessageTest { + + private static final String RDATE = "23-Jun-2004 06:26:26 -0700"; + private static final String ENV_DATE = + "\"Wed, 23 Jun 2004 18:56:42 +0530\""; + private static final String ENV_SUBJECT = "\"test\""; + private static final String ENV_UTF8_ENCODED_SUBJECT = + "=?UTF-8?B?VVRGOCB0ZXN0OiDgsqzgsr4g4LKH4LKy4LON4LKy4LK/IOCyuOCygg==?= " + + "=?UTF-8?B?4LKt4LK14LK/4LK44LOBIOCyh+CyguCypuCzhuCyqA==?= " + + "=?UTF-8?B?4LON4LKoIOCyueCzg+CypuCyr+CypuCysuCyvyA=?="; + private static final String ENV_ADDRS = + "((\"Jakarta Mail\" NIL \"testuser\" \"example.com\")) " + + "((\"Jakarta Mail\" NIL \"testuser\" \"example.com\")) " + + "((\"Jakarta Mail\" NIL \"testuser\" \"example.com\")) " + + "((NIL NIL \"testuser\" \"example.com\")) NIL NIL NIL " + + "\"<40D98512.9040803@example.com>\""; + private static final String ENVELOPE = + "(" + ENV_DATE + " " + ENV_SUBJECT + " " + ENV_ADDRS + ")"; + + public static abstract class IMAPTest { + public void init(Properties props) { + } + + ; + + public abstract void test(Folder folder, IMAPHandlerMessage handler) + throws Exception; + } + + /** + * Test that a small message size is returned correctly. + */ + @Test + public void testSizeSmall() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException { + Message m = folder.getMessage(1); + assertEquals(123, m.getSize()); + } + }, + new IMAPHandlerMessage() { + { + { + size = 123; + } + } + }); + } + + /** + * Test that a large message size is returned as Integer.MAX_VALUE + * from MimeMessage.getSize and returned as the actual value from + * IMAPMessage.getSizeLong. + */ + @Test + public void testSizeLarge() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException { + Message m = folder.getMessage(1); + assertEquals(Integer.MAX_VALUE, m.getSize()); + assertEquals((long) Integer.MAX_VALUE + 1, + ((IMAPMessage) m).getSizeLong()); + } + }, + new IMAPHandlerMessage() { + { + { + size = (long) Integer.MAX_VALUE + 1; + } + } + }); + } + + /** + * Test that returning NIL instead of an empty string for the content + * of the message works correctly. + */ + @Test + public void testEmptyBody() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty("mail.imap.partialfetch", "false"); + } + + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException, IOException { + Message m = folder.getMessage(1); + String t = (String) m.getContent(); + assertEquals("", t); + } + }, + new IMAPHandlerMessage() { + @Override + public void fetch(String line) throws IOException { + if (line.contains("BODYSTRUCTURE")) + untagged("1 FETCH (BODYSTRUCTURE " + + "(\"text\" \"plain\" (\"charset\" \"us-ascii\") " + + "NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL)" + + ")"); + else if (line.contains("BODY[TEXT]")) + untagged("1 FETCH (BODY[TEXT] NIL " + + "FLAGS (\\Seen \\Recent))"); + ok(); + } + }); + } + + @Test + public void testAttachementFileName() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerMessage handler) throws MessagingException, IOException { + Message m = folder.getMessage(1); + Multipart mp = (Multipart) m.getContent(); + BodyPart bp = mp.getBodyPart(1); + assertEquals("filename.csv", MimeUtility.decodeText(bp.getFileName())); + } + }, + new IMAPHandlerMessage() { + @Override + public void fetch(String line) throws IOException { + untagged("1 FETCH (BODYSTRUCTURE (" + + "(\"text\" \"html\" (\"charset\" \"utf-8\") NIL NIL \"base64\" 402 6 NIL NIL NIL NIL)" + + "(\"application\" \"octet-stream\" (\"name\" \"=?utf-8?B?ZmlsZW5hbWU=?= =?utf-8?B?LmNzdg==?=\") NIL NIL \"base64\" 658 NIL " + + "(\"attachment\" (\"filename\" \"\")) NIL NIL) \"mixed\" " + + "(\"boundary\" \"--boundary_539_27806e16-2599-4612-b98a-69335bedd206\") NIL NIL NIL))" + ); + ok(); + } + } + ); + } + + /** + * Test that returning NIL instead of an empty string for the content + * of an empty body part works correctly. + */ + @Test + public void testEmptyBodyAttachment() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty("mail.imap.partialfetch", "false"); + } + + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException, IOException { + Message m = folder.getMessage(1); + Multipart mp = (Multipart) m.getContent(); + BodyPart bp = mp.getBodyPart(1); + String t = (String) bp.getContent(); + assertEquals("", t); + } + }, + new IMAPHandlerMessage() { + @Override + public void fetch(String line) throws IOException { + if (line.contains("BODYSTRUCTURE")) + untagged("1 FETCH (BODYSTRUCTURE (" + + "(\"text\" \"plain\" (\"charset\" \"us-ascii\") " + + "NIL NIL \"7bit\" 4 0 NIL NIL NIL NIL)" + + "(\"text\" \"plain\" (\"charset\" \"us-ascii\") " + + "NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL)" + + " \"mixed\" (\"boundary\" \"----=_x\") NIL NIL))"); + else if (line.contains("BODY[2]")) + untagged("1 FETCH (BODY[2] NIL " + + "FLAGS (\\Seen \\Recent))"); + ok(); + } + }); + } + + /** + * Test that returning NIL instead of an empty string for the content + * of an empty body part works correctly. + * This is a bug in office365.com. Note the space in "base64 ". + */ + @Test + public void testBadEncoding() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty("mail.imap.partialfetch", "false"); + } + + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException, IOException { + Message m = folder.getMessage(1); + Multipart mp = (Multipart) m.getContent(); + BodyPart bp = mp.getBodyPart(1); + StringBuilder sb = new StringBuilder(); + try (InputStream is = bp.getInputStream()) { + int c; + while ((c = is.read()) != -1) + sb.append((char) c); + } + assertEquals("test", sb.toString()); + } + }, + new IMAPHandlerMessage() { + @Override + public void fetch(String line) throws IOException { + if (line.contains("BODYSTRUCTURE")) + untagged("1 FETCH (BODYSTRUCTURE (" + + "(\"text\" \"plain\" (\"charset\" \"us-ascii\") " + + "NIL NIL \"7bit\" 0 0 NIL NIL NIL NIL)" + + "(\"application\" \"octet-stream\" " + + "(\"name\" \"test.txt\") NIL NIL \"base64 \" " + + "8 NIL NIL NIL NIL) " + + "\"mixed\" (\"boundary\" \"=_x\") NIL NIL))"); + else if (line.contains("BODY[2]")) + untagged("1 FETCH (BODY[2] \"dGVzdA==\" " + + "FLAGS (\\Seen \\Recent))"); + ok(); + } + }); + } + + + /** + * Test that a UTF-8 encoded Subject is decoded properly. + */ + @Test + public void testUtf8SubjectEncoded() { + String s = null; + try { + s = MimeUtility.decodeText(ENV_UTF8_ENCODED_SUBJECT); + } catch (UnsupportedEncodingException ex) { + } + final String subject = s; + + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException { + Message m = folder.getMessage(1); + assertEquals(subject, m.getSubject()); + } + }, + new IMAPHandlerMessage() { + { + { + envelope = "(" + ENV_DATE + " \"" + + ENV_UTF8_ENCODED_SUBJECT + "\" " + + ENV_ADDRS + ")"; + } + } + }); + } + + /** + * Test that a UTF-8 Subject is decoded properly. + */ + @Test + public void testUtf8Subject() { + String s = null; + try { + s = MimeUtility.decodeText(ENV_UTF8_ENCODED_SUBJECT); + } catch (UnsupportedEncodingException ex) { + } + final String subject = s; + + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerMessage handler) + throws MessagingException { + Message m = folder.getMessage(1); + assertEquals(subject, m.getSubject()); + } + }, + new IMAPHandlerMessage() { + { + { + envelope = "(" + ENV_DATE + " \"" + subject + "\" " + + ENV_ADDRS + ")"; + capabilities += " ENABLE UTF8=ACCEPT"; + } + } + + @Override + public void enable(String line) throws IOException { + ok(); + } + }); + } + + private void testWithHandler(IMAPTest test, IMAPHandlerMessage handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + test.init(properties); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + test.test(folder, handler); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. + */ + private static class IMAPHandlerMessage extends IMAPHandler { + + String rdate = RDATE; + String envelope = ENVELOPE; + long size = 0; + + @Override + public void examine(String line) throws IOException { + numberOfMessages = 1; + super.examine(line); + } + + @Override + public void fetch(String line) throws IOException { + untagged("1 FETCH (ENVELOPE " + envelope + + " INTERNALDATE \"" + rdate + "\" " + + "RFC822.SIZE " + size + ")"); + ok(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPPlainHandler.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPPlainHandler.java new file mode 100644 index 0000000..a064e81 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPPlainHandler.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Handle IMAP connection with PLAIN authentication. + * + * @author Bill Shannon + */ +public class IMAPPlainHandler extends IMAPHandler { + + protected String username = "test"; + protected String password = "test"; + + public IMAPPlainHandler() { + capabilities += " LOGINDISABLED AUTH=PLAIN"; + } + + /** + * AUTHENTICATE PLAIN command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void authplain(String ir) throws IOException { + if (ir == null) { + cont(""); + ir = readLine(); + } + String auth = new String(Base64.getDecoder().decode( + ir.getBytes(StandardCharsets.US_ASCII)), + StandardCharsets.UTF_8); + String[] ap = auth.split("\000"); + String u = ap[1]; + String p = ap[2]; + //System.out.printf("USER: %s, PASSWORD: %s%n", u, p); + if (!u.equals(username) || !p.equals(password)) { + no("authentication failed"); + return; + } + ok("[CAPABILITY " + capabilities + "]"); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPResponseEventTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPResponseEventTest.java new file mode 100644 index 0000000..86b6d89 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPResponseEventTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.event.StoreEvent; +import jakarta.mail.event.StoreListener; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.imap.IMAPStore; +import org.xbib.net.mail.test.test.TestServer; + +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test IMAP response events. + */ +public final class IMAPResponseEventTest { + + private volatile boolean gotResponse; + + /** + * Test that response events are sent for the LOGIN command. + */ + @Test + public void testLoginResponseEvent() { + testLogin(""); + } + + /** + * Test that response events are sent for the AUTHENTICATE LOGIN command. + */ + @Test + public void testAuthLoginResponseEvent() { + testLogin("LOGINDISABLED AUTH=LOGIN"); + } + + /** + * Test that response events are sent for the AUTHENTICATE PLAIN command. + */ + @Test + public void testAuthPlainResponseEvent() { + testLogin("LOGINDISABLED AUTH=PLAIN"); + } + + private void testLogin(String type) { + TestServer server = null; + try { + final IMAPHandler handler = new IMAPHandlerLogin(type); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + properties.setProperty("mail.imap.enableresponseevents", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + final CountDownLatch latch = new CountDownLatch(1); + + final Store store = session.getStore("imap"); + store.addStoreListener(new StoreListener() { + @Override + public void notification(StoreEvent e) { + String s; + if (e.getMessageType() == IMAPStore.RESPONSE) { + s = "RESPONSE: "; + // is this the expected AUTHENTICATE response? + if (e.getMessage().contains("X-LOGIN-SUCCESS")) + gotResponse = true; + latch.countDown(); + } else + s = "OTHER: "; + //System.out.println(s + e.getMessage()); + } + }); + gotResponse = false; + try { + store.connect("test", "test"); + // time for event to be delivered + latch.await(5, TimeUnit.SECONDS); + assertTrue(gotResponse); + + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Forces use of specific login type and includes + * a fake capability to be included in the OK response that we + * will check for success. + */ + private static final class IMAPHandlerLogin extends IMAPHandler { + public IMAPHandlerLogin(String type) { + capabilities += " " + type + " X-LOGIN-SUCCESS"; + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSaslHandler.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSaslHandler.java new file mode 100644 index 0000000..9fdf3e1 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSaslHandler.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import org.xbib.net.mail.util.ASCIIUtility; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.sasl.AuthorizeCallback; +import javax.security.sasl.RealmCallback; +import javax.security.sasl.RealmChoiceCallback; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslException; +import javax.security.sasl.SaslServer; +import java.io.IOException; +import java.util.Base64; +import java.util.logging.Level; + +/** + * Handle IMAP connection with SASL authentication. + * + * @author Bill Shannon + */ +public class IMAPSaslHandler extends IMAPHandler { + + public IMAPSaslHandler() { + capabilities += " LOGINDISABLED AUTH=DIGEST-MD5"; + } + + /** + * AUTHENTICATE command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void authenticate(String mech, String ir) throws IOException { + final String u = "test"; + final String p = "test"; + final String realm = "test"; + + CallbackHandler cbh = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL callback length: " + callbacks.length); + for (int i = 0; i < callbacks.length; i++) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL callback " + i + ": " + callbacks[i]); + if (callbacks[i] instanceof NameCallback) { + NameCallback ncb = (NameCallback) callbacks[i]; + ncb.setName(u); + } else if (callbacks[i] instanceof PasswordCallback) { + PasswordCallback pcb = (PasswordCallback) callbacks[i]; + pcb.setPassword(p.toCharArray()); + } else if (callbacks[i] instanceof AuthorizeCallback) { + AuthorizeCallback ac = (AuthorizeCallback) callbacks[i]; + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL authorize: " + + "authn: " + ac.getAuthenticationID() + ", " + + "authz: " + ac.getAuthorizationID() + ", " + + "authorized: " + ac.getAuthorizedID()); + ac.setAuthorized(true); + } else if (callbacks[i] instanceof RealmCallback) { + RealmCallback rcb = (RealmCallback) callbacks[i]; + rcb.setText(realm != null ? + realm : rcb.getDefaultText()); + } else if (callbacks[i] instanceof RealmChoiceCallback) { + RealmChoiceCallback rcb = + (RealmChoiceCallback) callbacks[i]; + if (realm == null) + rcb.setSelectedIndex(rcb.getDefaultChoice()); + else { + // need to find specified realm in list + String[] choices = rcb.getChoices(); + for (int k = 0; k < choices.length; k++) { + if (choices[k].equals(realm)) { + rcb.setSelectedIndex(k); + break; + } + } + } + } + } + } + }; + + SaslServer ss; + try { + ss = Sasl.createSaslServer(mech, "imap", "localhost", null, cbh); + } catch (SaslException sex) { + LOGGER.log(Level.FINE, "Failed to create SASL server", sex); + no("Failed to create SASL server"); + return; + } + if (ss == null) { + LOGGER.fine("No SASL support"); + no("No SASL support"); + return; + } + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL server " + ss.getMechanismName()); + + byte[] response = new byte[0]; + while (!ss.isComplete()) { + try { + byte[] chal = ss.evaluateResponse(response); + if (ss.isComplete()) { + break; + } else { + // send challenge + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL challenge: " + + ASCIIUtility.toString(chal, 0, chal.length)); + byte[] ba = Base64.getEncoder().encode(chal); + if (ba.length > 0) + cont(ASCIIUtility.toString(ba, 0, ba.length)); + else + cont(); + // read response + String resp = readLine(); + response = resp.getBytes(); + response = Base64.getDecoder().decode(response); + } + } catch (SaslException ex) { + no(ex.toString()); + break; + } + } + + if (ss.isComplete() /*&& status == SUCCESS*/) { + String qop = (String) ss.getNegotiatedProperty(Sasl.QOP); + if (qop != null && (qop.equalsIgnoreCase("auth-int") || + qop.equalsIgnoreCase("auth-conf"))) { + // XXX - NOT SUPPORTED!!! + LOGGER.fine( + "SASL Mechanism requires integrity or confidentiality"); + no("SASL Mechanism requires integrity or confidentiality"); + return; + } + } + + ok("[CAPABILITY " + capabilities + "]"); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSaslLoginTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSaslLoginTest.java new file mode 100644 index 0000000..0568660 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSaslLoginTest.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.TestServer; + +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test login using a SASL mechanism. + */ +public final class IMAPSaslLoginTest { + + /** + * Test that login using a SASL mechanism works. + */ + @Test + public void testSaslLogin() { + TestServer server = null; + try { + IMAPHandler handler = new IMAPSaslHandler(); + server = new TestServer(handler); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + properties.setProperty("mail.imap.sasl.enable", "true"); + properties.setProperty("mail.imap.sasl.mechanisms", "DIGEST-MD5"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + // success! + } catch (MessagingException mex) { + fail("login failed"); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSearchTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSearchTest.java new file mode 100644 index 0000000..999c814 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPSearchTest.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.search.SearchException; +import jakarta.mail.search.SubjectTerm; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.imap.YoungerTerm; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test the search method. + */ +@Timeout(20) +public final class IMAPSearchTest { + + @Test + public void testWithinNotSupported() { + TestServer server = null; + try { + server = new TestServer(new IMAPHandler() { + @Override + public void search(String line) throws IOException { + bad("WITHIN not supported"); + } + }); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + properties.setProperty("mail.imap.throwsearchexception", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + Message[] msgs = folder.search(new YoungerTerm(1)); + fail("search didn't fail"); + } catch (SearchException ex) { + // success! + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Test that when the server supports UTF8 and the client enables it, + * the client doesn't issue a SEARCH CHARSET command even if the search + * term includes a non-ASCII character. + * (see RFC 6855, section 3, last paragraph) + */ + @Test + public void testUtf8Search() { + TestServer server = null; + try { + server = new TestServer(new IMAPUtf8Handler() { + @Override + public void search(String line) throws IOException { + if (line.contains("CHARSET")) + bad("CHARSET not supported"); + else + ok(); + } + }); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + Message[] msgs = folder.search(new SubjectTerm("\u2019")); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * An IMAPHandler that enables UTF-8 support. + */ + private static class IMAPUtf8Handler extends IMAPHandler { + { + { + capabilities += " ENABLE UTF8=ACCEPT"; + } + } + + @Override + public void enable(String line) throws IOException { + ok(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPStoreTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPStoreTest.java new file mode 100644 index 0000000..e0cfbad --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPStoreTest.java @@ -0,0 +1,438 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Folder; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.imap.IMAPStore; +import org.xbib.net.mail.test.test.TestServer; +import java.io.IOException; +import java.util.Properties; +import java.util.StringTokenizer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test IMAPStore methods. + */ +@Timeout(20) +public final class IMAPStoreTest { + + private static final String utf8Folder = "test\u03b1"; + private static final String utf7Folder = "test&A7E-"; + + public static abstract class IMAPTest { + public void init(Properties props) { + } + + ; + + public void test(Store store, TestServer server) throws Exception { + } + + ; + } + + /** + * Test that UTF-8 user name works with LOGIN authentication. + */ + @Test + public void testUtf8UsernameLogin() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect(utf8Folder, utf8Folder); + } + }, + new IMAPLoginHandler() { + @Override + public void authlogin(String ir) + throws IOException { + username = utf8Folder; + password = utf8Folder; + super.authlogin(ir); + } + }); + } + + /** + * Test that UTF-8 user name works with PLAIN authentication. + */ + @Test + public void testUtf8UsernamePlain() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect(utf8Folder, utf8Folder); + } + }, + new IMAPPlainHandler() { + @Override + public void authplain(String ir) + throws IOException { + username = utf8Folder; + password = utf8Folder; + super.authplain(ir); + } + }); + } + + /** + * Test that UTF-7 folder names in the NAMESPACE command are + * decoded properly. + */ + @Test + public void testUtf7Namespaces() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder[] pub = ((IMAPStore) store).getSharedNamespaces(); + assertEquals(utf8Folder, pub[0].getName()); + } + }, + new IMAPHandler() { + { + { + capabilities += " NAMESPACE"; + } + } + + @Override + public void namespace() throws IOException { + untagged("NAMESPACE ((\"\" \"/\")) ((\"~\" \"/\")) " + + "((\"" + utf7Folder + "/\" \"/\"))"); + ok(); + } + }); + } + + /** + * Test that using a UTF-8 folder name results in the proper UTF-8 + * unencoded name for the CREATE command. + */ + @Test + public void testUtf8FolderNameCreate() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder(utf8Folder); + assertTrue(test.create(Folder.HOLDS_MESSAGES)); + } + }, + new IMAPUtf8Handler() { + @Override + public void create(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip tag + st.nextToken(); // skip "CREATE" + String name = unquote(st.nextToken()); + if (name.equals(utf8Folder)) + ok(); + else + no("wrong name"); + } + + @Override + public void list(String line) throws IOException { + untagged("LIST (\\HasNoChildren) \"/\" \"" + + utf8Folder + "\""); + ok(); + } + }); + } + + /** + * Test that Store.close also closes open Folders. + */ + @Test + public void testCloseClosesFolder() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder("INBOX"); + test.open(Folder.READ_ONLY); + store.close(); + assertFalse(test.isOpen()); + assertEquals(1, server.clientCount()); + server.waitForClients(1); + // test will timeout if clients don't terminate + } + }, + new IMAPHandler() { + }); + } + + /** + * Test that Store.close closes connections in the pool. + */ + @Test + public void testCloseEmptiesPool() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty("mail.imap.connectionpoolsize", "2"); + } + + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder("INBOX"); + test.open(Folder.READ_ONLY); + Folder test2 = store.getFolder("INBOX"); + test2.open(Folder.READ_ONLY); + test.close(false); + test2.close(false); + store.close(); + assertEquals(2, server.clientCount()); + server.waitForClients(2); + // test will timeout if clients don't terminate + } + }, + new IMAPHandler() { + }); + } + + /** + * Test that Store failures don't close Folders. + */ + @Test + public void testStoreFailureDoesNotCloseFolder() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty( + "mail.imap.closefoldersonstorefailure", "false"); + } + + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder("INBOX"); + test.open(Folder.READ_ONLY); + try { + ((IMAPStore) store).getSharedNamespaces(); + fail("MessagingException expected"); + } catch (MessagingException mex) { + // expected + } + assertTrue(test.isOpen()); + store.close(); + assertFalse(test.isOpen()); + assertEquals(2, server.clientCount()); + server.waitForClients(2); + // test will timeout if clients don't terminate + } + }, + new IMAPHandler() { + { + { + capabilities += " NAMESPACE"; + } + } + + @Override + public void namespace() throws IOException { + exit(); + } + }); + } + + /** + * Test that Store.close after Store failure will close all Folders + * and empty the connectin pool. + */ + @Test + public void testCloseAfterFailure() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty( + "mail.imap.closefoldersonstorefailure", "false"); + } + + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder("INBOX"); + test.open(Folder.READ_ONLY); + try { + ((IMAPStore) store).getSharedNamespaces(); + fail("MessagingException expected"); + } catch (MessagingException mex) { + // expected + } + assertTrue(test.isOpen()); + test.close(); // put it back in the pool + store.close(); + assertEquals(2, server.clientCount()); + server.waitForClients(2); + // test will timeout if clients don't terminate + } + }, + new IMAPHandler() { + { + { + capabilities += " NAMESPACE"; + } + } + + @Override + public void namespace() throws IOException { + exit(); + } + }); + } + + /** + * Test that Store failures do close Folders. + */ + @Test + public void testStoreFailureDoesCloseFolder() { + testWithHandler( + new IMAPTest() { + @Override + public void init(Properties props) { + props.setProperty( + // the default, but just to be sure... + "mail.imap.closefoldersonstorefailure", "true"); + } + + @Override + public void test(Store store, TestServer server) + throws MessagingException, IOException { + store.connect("test", "test"); + Folder test = store.getFolder("INBOX"); + test.open(Folder.READ_ONLY); + try { + ((IMAPStore) store).getSharedNamespaces(); + fail("MessagingException expected"); + } catch (MessagingException mex) { + // expected + } + assertFalse(test.isOpen()); + store.close(); + assertEquals(2, server.clientCount()); + server.waitForClients(2); + // test will timeout if clients don't terminate + } + }, + new IMAPHandler() { + { + { + capabilities += " NAMESPACE"; + } + } + + @Override + public void namespace() throws IOException { + exit(); + } + }); + } + + private void testWithHandler(IMAPTest test, IMAPHandler handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + test.init(properties); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + try { + test.test(store, server); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + private static String unquote(String s) { + if (s.startsWith("\"") && s.endsWith("\"") && s.length() > 1) { + s = s.substring(1, s.length() - 1); + // check for any escaped characters + if (s.indexOf('\\') >= 0) { + StringBuilder sb = new StringBuilder(s.length()); // approx + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && i < s.length() - 1) + c = s.charAt(++i); + sb.append(c); + } + s = sb.toString(); + } + } + return s; + } + + /** + * An IMAPHandler that enables UTF-8 support. + */ + private static class IMAPUtf8Handler extends IMAPHandler { + { + { + capabilities += " ENABLE UTF8=ACCEPT"; + } + } + + @Override + public void enable(String line) throws IOException { + ok(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPUidExpungeTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPUidExpungeTest.java new file mode 100644 index 0000000..66463bd --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/IMAPUidExpungeTest.java @@ -0,0 +1,462 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.UIDFolder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Properties; +import java.util.StringTokenizer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + + +/** + * Test EXPUNGE responses during UID FETCH. + */ +@Timeout(20) +public final class IMAPUidExpungeTest { + + public static interface IMAPTest { + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException; + } + + @Test + public void testUIDSingle() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message m = ((UIDFolder) folder).getMessageByUID(2); + m.getFlags(); + assertEquals(1, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("1 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDSingle2() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message m = ((UIDFolder) folder).getMessageByUID(2); + m.getFlags(); + assertEquals(2, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDRange() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder) folder).getMessagesByUID(2, 4); + assertTrue(msgs[1] == null || msgs[1].isExpunged()); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 FETCH (UID 4)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDRange2() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder) folder).getMessagesByUID(2, 4); + assertTrue(msgs[1] == null || msgs[1].isExpunged()); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + untagged("3 FETCH (UID 4)"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDRange3() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder) folder).getMessagesByUID(2, 4); + // UID 3 is unknown and not returned + assertEquals(2, msgs.length); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[1].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + untagged("3 FETCH (UID 4)"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDRange4() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder) folder).getMessagesByUID(1, 3); + assertEquals(3, msgs.length); + msgs[0].getFlags(); + assertEquals(1, handler.getSeqNum()); + msgs[1].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("1 FETCH (UID 1)"); + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDRange5() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder) folder).getMessagesByUID(2, 4); + assertEquals(3, msgs.length); + msgs[0].getFlags(); + assertEquals(1, handler.getSeqNum()); + msgs[1].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 FETCH (UID 4)"); + untagged("1 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDList() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder) folder).getMessagesByUID( + new long[]{2, 3, 4}); + assertTrue(msgs[1] == null || msgs[1].isExpunged()); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 FETCH (UID 4)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDList2() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder) folder).getMessagesByUID( + new long[]{2, 3, 4}); + assertTrue(msgs[1] == null || msgs[1].isExpunged()); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + untagged("3 FETCH (UID 4)"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDList3() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder) folder).getMessagesByUID( + new long[]{2, 3, 4}); + assertTrue(msgs[1] == null || msgs[1].isExpunged()); + msgs[0].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 EXPUNGE"); + untagged("3 EXISTS"); + untagged("3 FETCH (UID 4)"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDList4() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder) folder).getMessagesByUID( + new long[]{1, 2, 3}); + assertEquals(3, msgs.length); + msgs[0].getFlags(); + assertEquals(1, handler.getSeqNum()); + msgs[1].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("1 FETCH (UID 1)"); + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + @Test + public void testUIDList5() { + testWithHandler( + new IMAPTest() { + @Override + public void test(Folder folder, IMAPHandlerExpunge handler) + throws MessagingException { + Message[] msgs = ((UIDFolder) folder).getMessagesByUID( + new long[]{2, 3, 4}); + assertEquals(3, msgs.length); + msgs[0].getFlags(); + assertEquals(1, handler.getSeqNum()); + msgs[1].getFlags(); + assertEquals(2, handler.getSeqNum()); + msgs[2].getFlags(); + assertEquals(3, handler.getSeqNum()); + } + }, + new IMAPHandlerExpunge() { + @Override + public void uidfetch(String line) throws IOException { + untagged("2 FETCH (UID 2)"); + untagged("3 FETCH (UID 3)"); + untagged("4 FETCH (UID 4)"); + untagged("1 EXPUNGE"); + untagged("3 EXISTS"); + numberOfMessages--; + ok(); + } + }); + } + + public void testWithHandler(IMAPTest test, IMAPHandlerExpunge handler) { + TestServer server = null; + try { + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("imap"); + Folder folder = null; + try { + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_WRITE); + test.test(folder, handler); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + if (folder != null) + folder.close(false); + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. + */ + private static class IMAPHandlerExpunge extends IMAPHandler { + // must be static because handler is cloned for each connection + private static int seqnum; + + @Override + public void select(String line) throws IOException { + numberOfMessages = 4; + super.select(line); + } + + @Override + public void fetch(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line, " "); + String tag = st.nextToken(); + String command = st.nextToken(); + seqnum = Integer.parseInt(st.nextToken()); + ok(); + } + + public int getSeqNum() { + return seqnum; + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/MessageCacheTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/MessageCacheTest.java new file mode 100644 index 0000000..3997771 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/MessageCacheTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.imap.MessageCache; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test the IMAP MessageCache. + */ +public class MessageCacheTest { + /** + * Test that when a message is expunged and a new message is added, + * the new message has the expected sequence number. + */ + @Test + public void testExpungeAdd() throws Exception { + // test a range of values to find boundary condition errors + for (int n = 1; n <= 100; n++) { + //System.out.println("MessageCache.testExpungeAdd: test " + n); + // start with one message + MessageCache mc = new MessageCache(1, false); + // add the remaining messages (eat into SLOP) + mc.addMessages(n - 1, 2); + // now expunge a message to cause the seqnums array to be created + mc.expungeMessage(1); + // and add one more message + mc.addMessages(1, n); + //System.out.println(" new seqnum " + mc.seqnumOf(n + 1)); + // does the new message have the expected sequence number? + assertEquals(mc.seqnumOf(n + 1), n); + } + } + + /** + * Test that when a message is expunged and new messages are added, + * the new messages have the expected sequence number. Similar to + * the above, but the seqnums array is created first, then expanded. + */ + @Test + public void testExpungeAddExpand() throws Exception { + // test a range of values to find boundary condition errors + for (int n = 2; n <= 100; n++) { + //System.out.println("MessageCache.testExpungeAdd: test " + n); + // start with two messages + MessageCache mc = new MessageCache(2, false); + // now expunge a message to cause the seqnums array to be created + mc.expungeMessage(1); + // add the remaining messages (eat into SLOP) + mc.addMessages(n - 1, 2); + //System.out.println(" new seqnum " + mc.seqnumOf(n + 1)); + // does the new message have the expected sequence number? + assertEquals(mc.seqnumOf(n + 1), n); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/BODYSTRUCTURETest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/BODYSTRUCTURETest.java new file mode 100644 index 0000000..abefc16 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/BODYSTRUCTURETest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2012, 2023 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 org.xbib.net.mail.test.imap.protocol; + +import jakarta.mail.internet.ParameterList; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.imap.protocol.BODYSTRUCTURE; +import org.xbib.net.mail.imap.protocol.FetchResponse; +import org.xbib.net.mail.imap.protocol.IMAPResponse; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Test the BODYSTRUCTURE class. + */ +public class BODYSTRUCTURETest { + /** + * Test workaround for Exchange bug that returns NIL instead of "" + * for a parameter with an empty value (name=""). + */ + @Test + public void testExchangeEmptyParameterValueBug() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 3 FETCH (BODYSTRUCTURE ((\"text\" \"plain\" (\"charset\" \"UTF-8\") " + + "NIL NIL \"quoted-printable\" 512 13 NIL (\"inline\" NIL) NIL NIL)" + + "(\"text\" \"html\" (\"charset\" \"UTF-8\") NIL NIL \"quoted-printable\" " + + "784 11 NIL (\"inline\" NIL) NIL NIL) \"alternative\" " + + "(\"boundary\" \"__139957996218379.example.com\" \"name\" NIL) NIL NIL))"); + // here's the incorrect NIL that should be "" ............^ + FetchResponse fr = new FetchResponse(response); + BODYSTRUCTURE bs = fr.getItem(BODYSTRUCTURE.class); + ParameterList p = bs.cParams; + assertNotNull(p.get("name")); + } + + /** + * Test workaround for Exchange bug that returns the Content-Description + * header value instead of the Content-Disposition for some kinds of + * (formerly S/MIME encrypted?) messages. + */ + @Test + public void testExchangeBadDisposition() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 1 FETCH (BODYSTRUCTURE (" + + "(\"text\" \"plain\" (\"charset\" \"us-ascii\") NIL NIL \"7bit\" " + + "21 0 NIL (\"inline\" NIL) NIL NIL)" + + "(\"application\" \"octet-stream\" (\"name\" \"private.txt\") " + + "NIL NIL \"base64\" 690 NIL " + + "(\"attachment\" (\"filename\" \"private.txt\")) NIL NIL) " + + "\"mixed\" (\"boundary\" \"----=_Part_0_-1731707885.1504253815584\") " + + "\"S/MIME Encrypted Message\" NIL))"); + // ^^^^^^^ here's the string that should be the disposition + FetchResponse fr = new FetchResponse(response); + BODYSTRUCTURE bs = fr.getItem(BODYSTRUCTURE.class); + assertEquals("S/MIME Encrypted Message", bs.description); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/EnvelopeTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/EnvelopeTest.java new file mode 100644 index 0000000..b9bc06a --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/EnvelopeTest.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2012, 2023 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 org.xbib.net.mail.test.imap.protocol; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.imap.protocol.FetchResponse; +import org.xbib.net.mail.imap.protocol.IMAPResponse; + +/** + * Test the ENVELOPE class. + */ +public class EnvelopeTest { + /** + * Test workaround for Yahoo IMAP bug that returns a bogus space + * character when one of the recipients is "undisclosed-recipients". + */ + @Test + public void testYahooUndisclosedRecipientsBug() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 2 FETCH (INTERNALDATE \"24-Apr-2012 20:28:58 +0000\" " + + "RFC822.SIZE 155937 " + + "ENVELOPE (\"Wed, 28 Sep 2011 11:16:17 +0100\" \"test\" " + + "((NIL NIL \"xxx\" \"tju.edu.cn\")) " + + "((NIL NIL \"xxx\" \"gmail.com\")) " + + "((NIL NIL \"xxx\" \"tju.edu.cn\")) " + + "((\"undisclosed-recipients\" NIL " + + "\"\\\"undisclosed-recipients\\\"\" NIL )) " + + // here's the space inserted by Yahoo IMAP ^ + "NIL NIL NIL " + + "\"\"))"); + FetchResponse fr = new FetchResponse(response); + // no exception means it worked + } + + /** + * Test workaround for Yahoo IMAP bug that returns an empty list + * instad of NIL for some addresses in ENVELOPE response. + */ + @Test + public void testYahooEnvelopeAddressListBug() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 2 FETCH (RFC822.SIZE 2567 INTERNALDATE \"29-Apr-2011 13:49:01 +0000\" " + + "ENVELOPE (\"Fri, 29 Apr 2011 19:19:01 +0530\" \"test\" " + + "((\"xxx\" NIL \"xxx\" \"milium.com.br\")) " + + "((\"xxx\" NIL \"xxx\" \"milium.com.br\")) " + + "((NIL NIL \"xxx\" \"live.hk\")) () NIL NIL NIL " + + "\"<20110429134718.70333732030A@mail2.milium.com.br>\"))"); + FetchResponse fr = new FetchResponse(response); + // no exception means it worked + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/IMAPProtocolTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/IMAPProtocolTest.java new file mode 100644 index 0000000..181a0b1 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/IMAPProtocolTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (c) 2014, 2023 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 org.xbib.net.mail.test.imap.protocol; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.imap.protocol.BODY; +import org.xbib.net.mail.imap.protocol.IMAPProtocol; +import org.xbib.net.mail.test.test.AsciiStringInputStream; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; + + +/** + * Test the IMAPProtocol class. + */ +public class IMAPProtocolTest { + private static final boolean debug = false; + private static final String content = "aXQncyBteSB0ZXN0IG1haWwNCg0K\r\n"; + private static final String response = + "* 1 FETCH (UID 127 BODY[1.1.MIME] {82}\r\n" + + "Content-Type: text/plain;\r\n" + + "\tcharset=\"utf-8\"\r\n" + + "Content-Transfer-Encoding: base64\r\n" + + "\r\n" + + " ENVELOPE (\"Mon, 17 Mar 2014 14:03:08 +0100\" \"test invoice\"" + + " ((\"Joe User\" NIL \"joe.user\" \"example.com\"))" + + " ((\"Joe User\" NIL \"joe.user\" \"example.com\"))" + + " ((\"Joe User\" NIL \"joe.user\" \"example.com\"))" + + " ((\"Joe User\" NIL \"joe.user\" \"example.com\"))" + + " NIL NIL NIL \"<1234@example.com>\") BODY[1.1]<0> " + + "{" + content.length() + "}\r\n" + content + + ")\r\n" + + "A0 OK FETCH completed.\r\n"; + + /** + * Test that a response containing multiple BODY elements + * returns the correct one. Derived from a customer bug + * with Exchange 2003. Normally this would never happen, + * but it's a valid IMAP response and Jakarta Mail needs to + * handle it properly. + */ + @Test + public void testMultipleBodyResponses() throws Exception { + Properties props = new Properties(); + props.setProperty("mail.imap.reusetagprefix", "true"); + IMAPProtocol p = new IMAPProtocol( + new AsciiStringInputStream(response), + new PrintStream(new ByteArrayOutputStream()), + props, + debug); + BODY b = p.fetchBody(1, "1.1"); + assertEquals("section number", "1.1", b.getSection()); + //System.out.println(b); + //System.out.write(b.getByteArray().getNewBytes()); + String result = new String(b.getByteArray().getNewBytes(), StandardCharsets.US_ASCII); + assertEquals("getByteArray.getNewBytes", content, result); + InputStream is = b.getByteArrayInputStream(); + byte[] ba = new byte[is.available()]; + is.read(ba); + result = new String(ba, StandardCharsets.US_ASCII); + assertEquals("getByteArrayInputStream", content, result); + } + + /** + * Same test as above, but using a different fetchBody method. + */ + @Test + public void testMultipleBodyResponses2() throws Exception { + Properties props = new Properties(); + props.setProperty("mail.imap.reusetagprefix", "true"); + IMAPProtocol p = new IMAPProtocol( + new AsciiStringInputStream(response), + new PrintStream(new ByteArrayOutputStream()), + props, + debug); + BODY b = p.fetchBody(1, "1.1", 0, content.length(), null); + assertEquals("section number", "1.1", b.getSection()); + //System.out.println(b); + //System.out.write(b.getByteArray().getNewBytes()); + String result = new String(b.getByteArray().getNewBytes(), StandardCharsets.US_ASCII); + assertEquals("getByteArray.getNewBytes", content, result); + InputStream is = b.getByteArrayInputStream(); + byte[] ba = new byte[is.available()]; + is.read(ba); + result = new String(ba, StandardCharsets.US_ASCII); + assertEquals("getByteArrayInputStream", content, result); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/MODSEQTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/MODSEQTest.java new file mode 100644 index 0000000..f2a3d82 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/MODSEQTest.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2015, 2023 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 org.xbib.net.mail.test.imap.protocol; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.imap.protocol.FetchResponse; +import org.xbib.net.mail.imap.protocol.IMAPResponse; +import org.xbib.net.mail.imap.protocol.MODSEQ; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test the MODSEQ class. + */ +public class MODSEQTest { + /** + * Test an example MODSEQ response. + */ + @Test + public void testAll() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 1 FETCH (MODSEQ (624140003))"); + FetchResponse fr = new FetchResponse(response); + MODSEQ m = fr.getItem(MODSEQ.class); + assertEquals(1, m.seqnum); + assertEquals(624140003, m.modseq); + } + + /** + * Test an example MODSEQ response with unnecessary spaces. + */ + @Test + public void testSpaces() throws Exception { + IMAPResponse response = new IMAPResponse( + "* 1 FETCH ( MODSEQ ( 624140003 ) )"); + FetchResponse fr = new FetchResponse(response); + MODSEQ m = fr.getItem(MODSEQ.class); + assertEquals(1, m.seqnum); + assertEquals(624140003, m.modseq); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/NamespacesTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/NamespacesTest.java new file mode 100644 index 0000000..389199b --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/NamespacesTest.java @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2015, 2023 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 org.xbib.net.mail.test.imap.protocol; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.imap.protocol.IMAPResponse; +import org.xbib.net.mail.imap.protocol.Namespaces; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test the Namespaces class. + */ +public class NamespacesTest { + private static final String utf8Folder = "#public\u03b1/"; + private static final String utf7Folder = "#public&A7E-/"; + + /** + * Test an example NAMESPACE response. + */ + @Test + public void testAll() throws Exception { + IMAPResponse response = new IMAPResponse( + "* NAMESPACE ((\"\" \"/\")) " + // personal + "((\"~\" \"/\")) " + // other users + "((\"#shared/\" \"/\")" + // shared + "(\"#public/\" \"/\")" + + "(\"#ftp/\" \"/\")" + + "(\"#news.\" \".\"))"); + Namespaces ns = new Namespaces(response); + assertEquals(1, ns.personal.length); + assertEquals("", ns.personal[0].prefix); + assertEquals('/', ns.personal[0].delimiter); + assertEquals(1, ns.otherUsers.length); + assertEquals("~", ns.otherUsers[0].prefix); + assertEquals('/', ns.otherUsers[0].delimiter); + assertEquals(4, ns.shared.length); + assertEquals("#shared/", ns.shared[0].prefix); + assertEquals('/', ns.shared[0].delimiter); + assertEquals("#public/", ns.shared[1].prefix); + assertEquals('/', ns.shared[1].delimiter); + assertEquals("#ftp/", ns.shared[2].prefix); + assertEquals('/', ns.shared[2].delimiter); + assertEquals("#news.", ns.shared[3].prefix); + assertEquals('.', ns.shared[3].delimiter); + } + + /** + * Test an example NAMESPACE response with unnecessary spaces. + */ + @Test + public void testSpaces() throws Exception { + IMAPResponse response = new IMAPResponse( + "* NAMESPACE ((\"\" \"/\")) " + // personal + "( ( \"~\" \"/\" ) ) " + // other users + "(( \"#shared/\" \"/\" )" + // shared + "( \"#public/\" \"/\" )" + + "( \"#ftp/\" \"/\" )" + + " (\"#news.\" \".\" ))"); + Namespaces ns = new Namespaces(response); + assertEquals(1, ns.personal.length); + assertEquals("", ns.personal[0].prefix); + assertEquals('/', ns.personal[0].delimiter); + assertEquals(1, ns.otherUsers.length); + assertEquals("~", ns.otherUsers[0].prefix); + assertEquals('/', ns.otherUsers[0].delimiter); + assertEquals(4, ns.shared.length); + assertEquals("#shared/", ns.shared[0].prefix); + assertEquals('/', ns.shared[0].delimiter); + assertEquals("#public/", ns.shared[1].prefix); + assertEquals('/', ns.shared[1].delimiter); + assertEquals("#ftp/", ns.shared[2].prefix); + assertEquals('/', ns.shared[2].delimiter); + assertEquals("#news.", ns.shared[3].prefix); + assertEquals('.', ns.shared[3].delimiter); + } + + /** + * Test a NAMESPACE response with a UTF-7 folder name. + */ + @Test + public void testUtf7() throws Exception { + IMAPResponse response = new IMAPResponse( + "* NAMESPACE ((\"\" \"/\")) " + // personal + "((\"~\" \"/\")) " + // other users + "((\"#shared/\" \"/\")" + // shared + "(\"" + utf7Folder + "\" \"/\")" + + "(\"#ftp/\" \"/\")" + + "(\"#news.\" \".\"))", + false); + Namespaces ns = new Namespaces(response); + assertEquals(1, ns.personal.length); + assertEquals("", ns.personal[0].prefix); + assertEquals('/', ns.personal[0].delimiter); + assertEquals(1, ns.otherUsers.length); + assertEquals("~", ns.otherUsers[0].prefix); + assertEquals('/', ns.otherUsers[0].delimiter); + assertEquals(4, ns.shared.length); + assertEquals("#shared/", ns.shared[0].prefix); + assertEquals('/', ns.shared[0].delimiter); + assertEquals(utf8Folder, ns.shared[1].prefix); + assertEquals('/', ns.shared[1].delimiter); + assertEquals("#ftp/", ns.shared[2].prefix); + assertEquals('/', ns.shared[2].delimiter); + assertEquals("#news.", ns.shared[3].prefix); + assertEquals('.', ns.shared[3].delimiter); + } + + /** + * Test a NAMESPACE response with a UTF-8 folder name. + */ + @Test + public void testUtf8() throws Exception { + IMAPResponse response = new IMAPResponse( + "* NAMESPACE ((\"\" \"/\")) " + // personal + "((\"~\" \"/\")) " + // other users + "((\"#shared/\" \"/\")" + // shared + "(\"" + utf8Folder + "\" \"/\")" + + "(\"#ftp/\" \"/\")" + + "(\"#news.\" \".\"))", + true); + Namespaces ns = new Namespaces(response); + assertEquals(1, ns.personal.length); + assertEquals("", ns.personal[0].prefix); + assertEquals('/', ns.personal[0].delimiter); + assertEquals(1, ns.otherUsers.length); + assertEquals("~", ns.otherUsers[0].prefix); + assertEquals('/', ns.otherUsers[0].delimiter); + assertEquals(4, ns.shared.length); + assertEquals("#shared/", ns.shared[0].prefix); + assertEquals('/', ns.shared[0].delimiter); + assertEquals(utf8Folder, ns.shared[1].prefix); + assertEquals('/', ns.shared[1].delimiter); + assertEquals("#ftp/", ns.shared[2].prefix); + assertEquals('/', ns.shared[2].delimiter); + assertEquals("#news.", ns.shared[3].prefix); + assertEquals('.', ns.shared[3].delimiter); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/StatusTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/StatusTest.java new file mode 100644 index 0000000..f3e70d2 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/StatusTest.java @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2015, 2023 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 org.xbib.net.mail.test.imap.protocol; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.iap.ParsingException; +import org.xbib.net.mail.imap.protocol.BASE64MailboxEncoder; +import org.xbib.net.mail.imap.protocol.IMAPResponse; +import org.xbib.net.mail.imap.protocol.Status; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test the Status class. + */ +public class StatusTest { + /** + * Test that the returned mailbox name is decoded. + */ + @Test + public void testMailboxDecode() throws Exception { + String mbox = "Entw\u00fcrfe"; + IMAPResponse response = new IMAPResponse( + "* STATUS " + + BASE64MailboxEncoder.encode(mbox) + + " (MESSAGES 231 UIDNEXT 44292)", false); + Status s = new Status(response); + assertEquals(mbox, s.mbox); + assertEquals(231, s.total); + assertEquals(44292, s.uidnext); + } + + /** + * Test that the returned mailbox name is correct when using UTF-8. + */ + @Test + public void testMailboxUtf8() throws Exception { + String mbox = "Entw\u00fcrfe"; + IMAPResponse response = new IMAPResponse( + "* STATUS " + + mbox + + " (MESSAGES 231 UIDNEXT 44292)", true); + Status s = new Status(response); + assertEquals(mbox, s.mbox); + assertEquals(231, s.total); + assertEquals(44292, s.uidnext); + } + + /** + * Test that spaces in the response don't confuse it. + */ + @Test + public void testSpaces() throws Exception { + IMAPResponse response = new IMAPResponse( + "* STATUS test ( MESSAGES 231 UIDNEXT 44292 )"); + Status s = new Status(response); + assertEquals("test", s.mbox); + assertEquals(231, s.total); + assertEquals(44292, s.uidnext); + } + + /** + * Test that a bad response throws a ParsingException + */ + @Test //(expected = ParsingException.class) + public void testBadResponseNoAttrList() throws Exception { + String mbox = "test"; + IMAPResponse response = new IMAPResponse("* STATUS test "); + Status s = new Status(response); + } + + /** + * Test that a bad response throws a ParsingException + */ + @Test // (expected = ParsingException.class) + public void testBadResponseNoAttrs() throws Exception { + String mbox = "test"; + IMAPResponse response = new IMAPResponse("* STATUS test ("); + Status s = new Status(response); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/StratoImapBugfixTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/StratoImapBugfixTest.java new file mode 100644 index 0000000..62790a0 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/StratoImapBugfixTest.java @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.imap.protocol; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.iap.ParsingException; +import org.xbib.net.mail.iap.Response; +import org.xbib.net.mail.imap.protocol.Status; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author tkrammer + */ +public class StratoImapBugfixTest { + @Test + public void testValidStatusResponseLeadingSpaces() throws Exception { + final Response response = + new Response("STATUS \" Sent Items \" (UIDNEXT 1)"); + final Status status = new Status(response); + + assertEquals(" Sent Items ", status.mbox); + assertEquals(1, status.uidnext); + } + + @Test + public void testValidStatusResponse() throws Exception { + final Response response = + new Response("STATUS \"Sent Items\" (UIDNEXT 1)"); + final Status status = new Status(response); + + assertEquals("Sent Items", status.mbox); + assertEquals(1, status.uidnext); + } + + @Test + public void testInvalidStatusResponse() throws Exception { + Response response = new Response("STATUS Sent Items (UIDNEXT 1)"); + final Status status = new Status(response); + + assertEquals("Sent Items", status.mbox); + assertEquals(1, status.uidnext); + } + + @Test + public void testMissingBracket() throws Exception { + final Response response = + new Response("STATUS \"Sent Items\" UIDNEXT 1)"); + + try { + new Status(response); + fail("Must throw exception"); + } catch (ParsingException e) { + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/UIDSetTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/UIDSetTest.java new file mode 100644 index 0000000..a89fc77 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/imap/protocol/UIDSetTest.java @@ -0,0 +1,232 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.imap.protocol; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.imap.protocol.UIDSet; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.StringTokenizer; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test UIDSet. + * + * @author Bill Shannon + */ + +//@RunWith(Parameterized.class) +public class UIDSetTest { + private TestData data; + + private static boolean gen_test_input = false; // output good + private static int errors = 0; // number of errors detected + + private static boolean junit; + + static class TestData { + public String name; + public String uids; + public long max; + public String maxuids; + public long[] expect; + } + + public UIDSetTest(TestData t) { + data = t; + } + + @Test + public void testData() { + test(data); + } + + //@Parameters + public static Collection data() throws Exception { + junit = true; + // XXX - gratuitous array requirement + List testData = new ArrayList<>(); + BufferedReader in = new BufferedReader(new InputStreamReader( + UIDSetTest.class.getResourceAsStream("uiddata"))); + TestData t; + while ((t = parse(in)) != null) + testData.add(new TestData[]{t}); + return testData; + } + + public static void main(String[] argv) throws Exception { + int optind; + // XXX - all options currently ignored + for (optind = 0; optind < argv.length; optind++) { + if (argv[optind].equals("-")) { + // ignore + } else if (argv[optind].equals("-g")) { + gen_test_input = true; + } else if (argv[optind].equals("--")) { + optind++; + break; + } else if (argv[optind].startsWith("-")) { + System.out.println( + "Usage: uidtest [-g] [-]"); + System.exit(1); + } else { + break; + } + } + + // read from stdin + BufferedReader in = + new BufferedReader(new InputStreamReader(System.in)); + + TestData t; + while ((t = parse(in)) != null) + test(t); + System.exit(errors); + } + + /* + * Parse the input, returning a test case. + */ + public static TestData parse(BufferedReader in) throws Exception { + + String line = null; + for (; ; ) { + line = in.readLine(); + if (line == null) + return null; + if (line.length() == 0 || line.startsWith("#")) + continue; + + if (!line.startsWith("TEST")) + throw new Exception("Bad test data format"); + break; + } + + TestData t = new TestData(); + int i = line.indexOf(' '); // XXX - crude + t.name = line.substring(i + 1); + + line = in.readLine(); + StringTokenizer st = new StringTokenizer(line); + String tok = st.nextToken(); + if (!tok.equals("DATA")) + throw new Exception("Bad test data format: " + line); + tok = st.nextToken(); + if (tok.equals("NULL")) + t.uids = null; + else if (tok.equals("EMPTY")) + t.uids = ""; + else + t.uids = tok; + + line = in.readLine(); + st = new StringTokenizer(line); + tok = st.nextToken(); + if (tok.equals("MAX")) { + tok = st.nextToken(); + try { + t.max = Long.valueOf(tok); + } catch (NumberFormatException ex) { + throw new Exception("Bad MAX value in line: " + line); + } + if (st.hasMoreTokens()) + t.maxuids = st.nextToken(); + else + t.maxuids = t.uids; + line = in.readLine(); + st = new StringTokenizer(line); + tok = st.nextToken(); + } + List uids = new ArrayList<>(); + if (!tok.equals("EXPECT")) + throw new Exception("Bad test data format: " + line); + while (st.hasMoreTokens()) { + tok = st.nextToken(); + if (tok.equals("NULL")) + t.expect = null; + else if (tok.equals("EMPTY")) + t.expect = new long[0]; + else { + try { + uids.add(Long.valueOf(tok)); + } catch (NumberFormatException ex) { + throw new Exception("Bad DATA option in line: " + line); + } + } + } + if (uids.size() > 0) { + t.expect = new long[uids.size()]; + i = 0; + for (Long l : uids) + t.expect[i++] = l.longValue(); + } + + return t; + } + + /** + * Test the data in the test case. + */ + public static void test(TestData t) { + // XXX - handle nulls + + // first, test string to array + UIDSet[] uidset = UIDSet.parseUIDSets(t.uids); + long[] uids; + if (t.max > 0) + uids = UIDSet.toArray(uidset, t.max); + else + uids = UIDSet.toArray(uidset); + if (junit) + assertArrayEquals(t.expect, uids); + else if (!arrayEquals(t.expect, uids)) { + System.out.println("Test: " + t.name); + System.out.println("FAIL"); + errors++; + } + + // now, test the reverse + UIDSet[] uidset2 = UIDSet.createUIDSets(uids); + String suid = UIDSet.toString(uidset2); + String euid = t.max > 0 ? t.maxuids : t.uids; + if (junit) + assertEquals(euid, suid); + else if (!euid.equals(suid)) { + System.out.println("Test: " + t.name); + System.out.println("FAIL2"); + errors++; + } + } + + private static boolean arrayEquals(long[] a, long[] b) { + if (a == b) + return true; + if (a == null || b == null) + return false; + if (a.length != b.length) + return false; + for (int i = 0; i < a.length; i++) + if (a[i] != b[i]) + return false; + return true; + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3AuthDebugTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3AuthDebugTest.java new file mode 100644 index 0000000..229cce2 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3AuthDebugTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.pop3; + +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that authentication information is only included in + * the debug output when explicitly requested by setting the + * property "mail.debug.auth" to "true". + * + * XXX - should test all authentication types, but that requires + * more work in the dummy test server. + */ +@Timeout(20) +public final class POP3AuthDebugTest { + + /** + * Test that authentication information isn't included in the debug output. + */ + @Test + public void testNoAuthDefault() { + final Properties properties = new Properties(); + assertFalse(test(properties, "PASS")); + } + + @Test + public void testNoAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "false"); + assertFalse(test(properties, "PASS")); + } + + /** + * Test that authentication information *is* included in the debug output. + */ + @Test + public void testAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "true"); + assertTrue(test(properties, "PASS")); + } + + /** + * Create a test server, connect to it, and collect the debug output. + * Scan the debug output looking for "expect", return true if found. + */ + public boolean test(Properties properties, String expect) { + TestServer server = null; + try { + final POP3Handler handler = new POP3Handler(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(bos); + session.setDebugOut(ps); + session.setDebug(true); + + final Store store = session.getStore("pop3"); + try { + store.connect("test", "test"); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + + ps.close(); + bos.close(); + ByteArrayInputStream bis = + new ByteArrayInputStream(bos.toByteArray()); + BufferedReader r = new BufferedReader( + new InputStreamReader(bis, StandardCharsets.US_ASCII)); + String line; + boolean found = false; + while ((line = r.readLine()) != null) { + if (line.startsWith("DEBUG")) + continue; + if (line.startsWith("*")) + continue; + if (line.contains(expect)) + found = true; + } + r.close(); + return found; + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + return false; // XXX - doesn't matter + } finally { + if (server != null) { + server.quit(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3FolderClosedExceptionTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3FolderClosedExceptionTest.java new file mode 100644 index 0000000..9a6d486 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3FolderClosedExceptionTest.java @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.pop3; + +import jakarta.mail.Folder; +import jakarta.mail.FolderClosedException; +import jakarta.mail.Message; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that FolderClosedException is thrown when server times out connection. + * This test is derived from real failures seen with Hotmail. + * + * @author sbo + * @author Bill Shannon + */ +public final class POP3FolderClosedExceptionTest { + + /** + * Test that FolderClosedException is thrown when the timeout occurs + * when reading the message body. + */ + @Test + public void testFolderClosedExceptionBody() { + TestServer server = null; + try { + final POP3Handler handler = new POP3HandlerTimeoutBody(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("pop3"); + try { + store.connect("test", "test"); + final Folder folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + Message msg = folder.getMessage(1); + try { + msg.getContent(); + } catch (IOException ioex) { + // expected + // first attempt detects error return from server + } + // second attempt detects closed connection from server + msg.getContent(); + + // Check + assertFalse(folder.isOpen()); + } catch (FolderClosedException ex) { + // success! + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Returns ERR for RETR the first time, + * then closes the connection the second time. + */ + private static final class POP3HandlerTimeoutBody extends POP3Handler { + + private boolean first = true; + + /** + * {@inheritDoc} + */ + @Override + public void retr(String arg) throws IOException { + if (first) { + println("-ERR Server timeout"); + first = false; + } else + exit(); + } + } + + /** + * Test that FolderClosedException is thrown when the timeout occurs + * when reading the headers. + */ + @Test + public void testFolderClosedExceptionHeaders() { + TestServer server = null; + try { + final POP3Handler handler = new POP3HandlerTimeoutHeader(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("pop3"); + try { + store.connect("test", "test"); + final Folder folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + Message msg = folder.getMessage(1); + msg.getSubject(); + + // Check + assertFalse(folder.isOpen()); + } catch (FolderClosedException ex) { + // success! + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler. Returns ERR for TOP, then closes connection. + */ + private static final class POP3HandlerTimeoutHeader extends POP3Handler { + + /** + * {@inheritDoc} + */ + @Override + public void top(String arg) throws IOException { + println("-ERR Server timeout"); + exit(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3Handler.java b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3Handler.java new file mode 100644 index 0000000..6ac60a4 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3Handler.java @@ -0,0 +1,289 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.pop3; + +import org.xbib.net.mail.test.test.ProtocolHandler; + +import java.io.IOException; +import java.util.StringTokenizer; +import java.util.logging.Level; + +/** + * Handle connection. + * + * @author sbo + */ +public class POP3Handler extends ProtocolHandler { + + /** + * Current line. + */ + private String currentLine; + + /** + * First test message. + */ + private String top1 = + "Mime-Version: 1.0\r\n" + + "From: joe@example.com\r\n" + + "To: bob@example.com\r\n" + + "Subject: Example\r\n" + + "Content-Type: text/plain\r\n" + + "\r\n"; + private String msg1 = top1 + + "plain text\r\n"; + + /** + * Second test message. + */ + private String top2 = + "Mime-Version: 1.0\r\n" + + "From: joe@example.com\r\n" + + "To: bob@example.com\r\n" + + "Subject: Multipart Example\r\n" + + "Content-Type: multipart/mixed; boundary=\"xxx\"\r\n" + + "\r\n"; + private String msg2 = top2 + + "preamble\r\n" + + "--xxx\r\n" + + "\r\n" + + "first part\r\n" + + "\r\n" + + "--xxx\r\n" + + "\r\n" + + "second part\r\n" + + "\r\n" + + "--xxx--\r\n"; + + /** + * Send greetings. + * + * @throws IOException unable to write to socket + */ + @Override + public void sendGreetings() throws IOException { + this.println("+OK POP3 CUSTOM"); + } + + /** + * Send String to socket. + * + * @param str String to send + * @throws IOException unable to write to socket + */ + public void println(final String str) throws IOException { + this.writer.print(str); + this.writer.print("\r\n"); + this.writer.flush(); + } + + /** + * Handle command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void handleCommand() throws IOException { + this.currentLine = readLine(); + + if (this.currentLine == null) { + // probably just EOF because the socket was closed + //LOGGER.severe("Current line is null!"); + this.exit(); + return; + } + + final StringTokenizer st = new StringTokenizer(this.currentLine, " "); + final String commandName = st.nextToken().toUpperCase(); + final String arg = st.hasMoreTokens() ? st.nextToken() : null; + if (commandName == null) { + LOGGER.severe("Command name is empty!"); + this.exit(); + return; + } + + if (commandName.equals("STAT")) { + this.stat(); + } else if (commandName.equals("LIST")) { + this.list(); + } else if (commandName.equals("RETR")) { + this.retr(arg); + } else if (commandName.equals("DELE")) { + this.dele(); + } else if (commandName.equals("NOOP")) { + this.noop(); + } else if (commandName.equals("RSET")) { + this.rset(); + } else if (commandName.equals("QUIT")) { + this.quit(); + } else if (commandName.equals("TOP")) { + this.top(arg); + } else if (commandName.equals("UIDL")) { + this.uidl(); + } else if (commandName.equals("USER")) { + this.user(); + } else if (commandName.equals("PASS")) { + this.pass(); + } else if (commandName.equals("CAPA")) { + this.capa(); + } else if (commandName.equals("AUTH")) { + this.auth(); + } else { + LOGGER.log(Level.SEVERE, "ERROR command unknown: {0}", commandName); + this.println("-ERR unknown command"); + } + } + + /** + * STAT command. + * + * @throws IOException unable to read/write to socket + */ + public void stat() throws IOException { + this.println("+OK 2 " + (msg1.length() + msg2.length())); + } + + /** + * LIST command. + * + * @throws IOException unable to read/write to socket + */ + public void list() throws IOException { + this.writer.println("+OK"); + this.writer.println("1 " + msg1.length()); + this.writer.println("2 " + msg2.length()); + this.println("."); + } + + /** + * RETR command. + * + * @throws IOException unable to read/write to socket + */ + public void retr(String arg) throws IOException { + String msg; + if (arg.equals("1")) + msg = msg1; + else + msg = msg2; + this.println("+OK " + msg.length() + " octets"); + this.writer.write(msg); + this.println("."); + } + + /** + * DELE command. + * + * @throws IOException unable to read/write to socket + */ + public void dele() throws IOException { + this.println("-ERR DELE not supported"); + } + + /** + * NOOP command. + * + * @throws IOException unable to read/write to socket + */ + public void noop() throws IOException { + this.println("+OK"); + } + + /** + * RSET command. + * + * @throws IOException unable to read/write to socket + */ + public void rset() throws IOException { + this.println("+OK"); + } + + /** + * QUIT command. + * + * @throws IOException unable to read/write to socket + */ + public void quit() throws IOException { + this.println("+OK"); + this.exit(); + } + + /** + * TOP command. + * XXX - ignores number of lines argument + * + * @throws IOException unable to read/write to socket + */ + public void top(String arg) throws IOException { + String top; + if (arg.equals("1")) + top = top1; + else + top = top2; + this.println("+OK " + top.length() + " octets"); + this.writer.write(top); + this.println("."); + } + + /** + * UIDL command. + * + * @throws IOException unable to read/write to socket + */ + public void uidl() throws IOException { + this.writer.println("+OK"); + this.writer.println("1 1"); + this.writer.println("2 2"); + this.println("."); + } + + /** + * USER command. + * + * @throws IOException unable to read/write to socket + */ + public void user() throws IOException { + this.println("+OK"); + } + + /** + * PASS command. + * + * @throws IOException unable to read/write to socket + */ + public void pass() throws IOException { + this.println("+OK"); + } + + /** + * CAPA command + * + * @throws IOException unable to write to socket + */ + public void capa() throws IOException { + this.println("-ERR CAPA not supported"); + } + + /** + * AUTH command + * + * @throws IOException unable to write to socket + */ + public void auth() throws IOException { + this.println("-ERR AUTH not supported"); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3MessageTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3MessageTest.java new file mode 100644 index 0000000..76aae04 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3MessageTest.java @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.pop3; + +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Multipart; +import jakarta.mail.Part; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.TestServer; + +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that we can read POP3 messages. + * + * @author Bill Shannon + */ +public final class POP3MessageTest { + + private static TestServer server = null; + private static Store store; + private static Folder folder; + + private static void startServer(boolean cached) { + try { + final POP3Handler handler = new POP3Handler(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", String.valueOf(server.getPort())); + if (cached) + properties.setProperty("mail.pop3.filecache.enable", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + store = session.getStore("pop3"); + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + private static void stopServer() { + try { + if (folder != null) + folder.close(false); + if (store != null) + store.close(); + } catch (MessagingException ex) { + // ignore it + } finally { + if (server != null) + server.quit(); + } + } + + /** + * Test that we can read the content of a message twice. + * A bug caused POP3Message to return the same stream the + * second time, instead of a new stream positioned at the + * beginning of the data. This caused multipart parsing + * to fail. + */ + @Test + public void testReadTwice() throws Exception { + readTwice(false); + } + + /** + * Now test it using the file cache. + */ + @Test + public void testReadTwiceCached() throws Exception { + readTwice(true); + } + + private void readTwice(boolean cached) throws Exception { + startServer(cached); + try { + Message[] msgs = folder.getMessages(); + for (int i = 0; i < msgs.length; i++) { + loadMail(msgs[i]); + loadMail(msgs[i]); + } + } finally { + stopServer(); + } + // no exception is success! + } + + private void loadMail(Part p) throws Exception { + Object content = p.getContent(); + if (content instanceof Multipart) { + Multipart mp = (Multipart) content; + int cnt = mp.getCount(); + for (int i = 0; i < cnt; i++) + loadMail(mp.getBodyPart(i)); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3ReadableMimeTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3ReadableMimeTest.java new file mode 100644 index 0000000..371ea4c --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3ReadableMimeTest.java @@ -0,0 +1,136 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.pop3; + +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Part; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.TestServer; +import org.xbib.net.mail.util.ReadableMime; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test ReadableMime support for POP3. + * + * @author Bill Shannon + */ +public final class POP3ReadableMimeTest { + + private static TestServer server = null; + private static Store store; + private static Folder folder; + + private static void startServer(boolean cached) { + try { + final POP3Handler handler = new POP3Handler(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", String.valueOf(server.getPort())); + if (cached) + properties.setProperty("mail.pop3.filecache.enable", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + store = session.getStore("pop3"); + store.connect("test", "test"); + folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } + + private static void stopServer() { + try { + if (folder != null) + folder.close(false); + if (store != null) + store.close(); + } catch (MessagingException ex) { + // ignore it + } finally { + if (server != null) + server.quit(); + } + } + + /** + * Test that the data returned by the getMimeStream method + * is exactly the same data as produced by the writeTo method. + */ + @Test + public void testReadableMime() throws Exception { + test(false); + } + + /** + * Now test it using the file cache. + */ + @Test + public void testReadableMimeCached() throws Exception { + test(true); + } + + private void test(boolean cached) throws Exception { + startServer(cached); + try { + Message[] msgs = folder.getMessages(); + for (int i = 0; i < msgs.length; i++) + verifyData(msgs[i]); + } finally { + stopServer(); + } + // no exception is success! + } + + private void verifyData(Part p) throws MessagingException, IOException { + assertTrue(p instanceof ReadableMime); + InputStream is = null; + try { + ReadableMime rp = (ReadableMime) p; + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + p.writeTo(bos); + bos.close(); + byte[] buf = bos.toByteArray(); + is = rp.getMimeStream(); + int i, b; + for (i = 0; (b = is.read()) != -1; i++) + assertTrue(b == (buf[i] & 0xff)); + assertTrue(i == buf.length); + } finally { + try { + is.close(); + } catch (IOException ex) { + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3StoreTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3StoreTest.java new file mode 100644 index 0000000..8e70e95 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/pop3/POP3StoreTest.java @@ -0,0 +1,288 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.pop3; + +import jakarta.mail.AuthenticationFailedException; +import jakarta.mail.Folder; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.pop3.POP3Store; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test POP3Store. + * + * @author sbo + * @author Bill Shannon + */ +public final class POP3StoreTest { + + /** + * Check is connected. + */ + @Test + public void testIsConnected() { + TestServer server = null; + try { + final POP3Handler handler = new POP3HandlerNoopErr(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("pop3"); + try { + store.connect("test", "test"); + final Folder folder = store.getFolder("INBOX"); + folder.open(Folder.READ_ONLY); + + // Check + assertFalse(folder.isOpen()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Check that enabling APOP with a server that doesn't support APOP + * (and doesn't return any information in the greeting) doesn't fail. + */ + @Test + public void testApopNotSupported() { + TestServer server = null; + try { + final POP3Handler handler = new POP3HandlerNoGreeting(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", String.valueOf(server.getPort())); + properties.setProperty("mail.pop3.apop.enable", "true"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Store store = session.getStore("pop3"); + try { + store.connect("test", "test"); + // success! + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Check whether POP3 XOAUTH2 connection can be established using single line authentication format (default) + */ + @Test + public void testXOAUTH2POP3Connection() { + TestServer server = null; + + try { + final POP3Handler handler = new POP3HandlerXOAUTH(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", String.valueOf(server.getPort())); + properties.setProperty("mail.pop3.auth.mechanisms", "XOAUTH2"); + + final Session session = Session.getInstance(properties); + + final POP3Store store = (POP3Store) session.getStore("pop3"); + try { + store.protocolConnect("localhost", server.getPort(), "test", "test"); + } catch (Exception ex) { + System.out.println(ex); + ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Check whether POP3 XOAUTH2 connection can be established using single line authentication format + * when the authentication format has ben set + * using: mail.pop3.auth.xoauth2.two.line.authentication.format property + */ + @Test + public void testXOAUTH2POP3ConnectionWithSingleLineAuthenticationFlag() { + TestServer server = null; + + try { + final POP3Handler handler = new POP3HandlerXOAUTH(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", String.valueOf(server.getPort())); + properties.setProperty("mail.pop3.auth.mechanisms", "XOAUTH2"); + properties.setProperty("mail.pop3.disablecapa", "false"); + properties.setProperty("mail.pop3.auth.xoauth2.two.line.authentication.format", "false"); + + final Session session = Session.getInstance(properties); + + final POP3Store store = (POP3Store) session.getStore("pop3"); + try { + store.protocolConnect("localhost", server.getPort(), "test", "test"); + } catch (Exception ex) { + System.out.println(ex); + ex.printStackTrace(); + fail(ex.toString()); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Check whether POP3 XOAUTH2 authentication method is invoked using two line authentication format + * using: mail.pop3.auth.xoauth2.two.line.authentication.format property + */ + @Test + public void testXOAUTH2POP3ConnectionWithTwoLineAuthenticationFlag() { + TestServer server = null; + + try { + final POP3Handler handler = new POP3HandlerXOAUTH(); + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.pop3.host", "localhost"); + properties.setProperty("mail.pop3.port", String.valueOf(server.getPort())); + properties.setProperty("mail.pop3.auth.mechanisms", "XOAUTH2"); + properties.setProperty("mail.pop3.disablecapa", "false"); + properties.setProperty("mail.pop3.auth.xoauth2.two.line.authentication.format", "true"); + + final Session session = Session.getInstance(properties); + + final POP3Store store = (POP3Store) session.getStore("pop3"); + try { + store.protocolConnect("localhost", server.getPort(), "test", "test"); + } catch (Exception ex) { + assertTrue(ex instanceof AuthenticationFailedException); + assertTrue(ex.toString().contains("unknown command")); + } finally { + store.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Custom handler of AUTH command. + * + * @author Mateusz Marzęcki + */ + private static class POP3HandlerXOAUTH extends POP3Handler { + @Override + public void auth() throws IOException { + this.println("+OK POP3 server ready"); + } + + @Override + public void capa() throws IOException { + this.writer.println("+OK"); + this.writer.println("SASL PLAIN XOAUTH2"); + this.println("."); + } + } + + /** + * Custom handler. Returns ERR for NOOP. + * + * @author sbo + */ + private static final class POP3HandlerNoopErr extends POP3Handler { + + /** + * {@inheritDoc} + */ + @Override + public void noop() throws IOException { + this.println("-ERR"); + } + } + + /** + * Custom handler. Don't include any extra information in the greeting. + * + * @author Bill Shannon + */ + private static final class POP3HandlerNoGreeting extends POP3Handler { + + /** + * {@inheritDoc} + */ + @Override + public void sendGreetings() throws IOException { + this.println("+OK"); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/NopServer.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/NopServer.java new file mode 100644 index 0000000..c253e05 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/NopServer.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import java.io.IOException; +import java.io.InputStream; +import java.net.ServerSocket; +import java.net.Socket; + +/** + * A server that does nothing, but keeps track of + * whether the client socket was closed. + * + * Inspired by, and derived from, POP3Server by sbo. + * + * @author Bill Shannon + */ +public final class NopServer extends Thread { + + /** + * Server socket. + */ + private ServerSocket serverSocket; + + /** + * Keep on? + */ + private volatile boolean keepOn; + + /** + * Did we get EOF on the client socket? + */ + private volatile boolean gotEOF = false; + + /** + * Nop server. + */ + public NopServer() throws IOException { + serverSocket = new ServerSocket(0); + } + + /** + * Return the port the server is listening on. + */ + public int getPort() { + return serverSocket.getLocalPort(); + } + + /** + * Exit Nop server. + */ + public void quit() { + try { + keepOn = false; + if (serverSocket != null && !serverSocket.isClosed()) { + serverSocket.close(); + serverSocket = null; + } + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + public boolean eof() { + return gotEOF; + } + + @Override + public void run() { + try { + keepOn = true; + + while (keepOn) { + try { + final Socket clientSocket = serverSocket.accept(); + /* + * Do nothing but consume any input and throw it away. + * When we see EOF, remember it. + */ + InputStream is = clientSocket.getInputStream(); + while (is.read() >= 0) + ; + gotEOF = true; + } catch (final IOException e) { + //e.printStackTrace(); + } + } + } finally { + quit(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPAuthDebugTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPAuthDebugTest.java new file mode 100644 index 0000000..df91b62 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPAuthDebugTest.java @@ -0,0 +1,132 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import jakarta.mail.Session; +import jakarta.mail.Transport; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that authentication information is only included in + * the debug output when explicitly requested by setting the + * property "mail.debug.auth" to "true". + * + * XXX - should test all authentication types, but that requires + * more work in the dummy test server. + */ +@Timeout(20) +public final class SMTPAuthDebugTest { + + /** + * Test that authentication information isn't included in the debug output. + */ + @Test + public void testNoAuthDefault() { + final Properties properties = new Properties(); + assertFalse(test(properties, "AUTH")); + } + + @Test + public void testNoAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "false"); + assertFalse(test(properties, "AUTH")); + } + + /** + * Test that authentication information *is* included in the debug output. + */ + @Test + public void testAuth() { + final Properties properties = new Properties(); + properties.setProperty("mail.debug.auth", "true"); + assertTrue(test(properties, "AUTH")); + } + + /** + * Create a test server, connect to it, and collect the debug output. + * Scan the debug output looking for "expect", return true if found. + */ + public boolean test(Properties properties, String expect) { + TestServer server = null; + try { + final SMTPHandler handler = new SMTPHandler(); + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintStream ps = new PrintStream(bos); + session.setDebugOut(ps); + session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + } catch (Exception ex) { + System.out.println(ex); + //ex.printStackTrace(); + fail(ex.toString()); + } finally { + t.close(); + } + + ps.close(); + bos.close(); + ByteArrayInputStream bis = + new ByteArrayInputStream(bos.toByteArray()); + BufferedReader r = new BufferedReader( + new InputStreamReader(bis, StandardCharsets.US_ASCII)); + String line; + boolean found = false; + while ((line = r.readLine()) != null) { + if (line.startsWith("DEBUG")) + continue; + if (line.startsWith("*")) + continue; + if (line.startsWith(expect)) + found = true; + } + r.close(); + return found; + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + return false; // XXX - doesn't matter + } finally { + if (server != null) { + server.quit(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPBdatTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPBdatTest.java new file mode 100644 index 0000000..863ca12 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPBdatTest.java @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.TestServer; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test the BDAT command. + */ +public final class SMTPBdatTest { + + private byte[] message; + + @Test + public void testBdatSuccess() throws Exception { + TestServer server = null; + try { + SMTPHandler handler = new SMTPHandler() { + { + { + extensions.add("CHUNKING"); + } + } + + @Override + public void setMessage(byte[] msg) { + message = msg; + } + }; + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.smtp.chunksize", "128"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + try { + MimeMessage msg = new MimeMessage(session); + msg.setRecipients(Message.RecipientType.TO, "joe@example.com"); + msg.setSubject("test"); + msg.setText("test\r\n"); + t.connect(); + t.sendMessage(msg, msg.getAllRecipients()); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + msg.writeTo(bos); + bos.close(); + byte[] orig = bos.toByteArray(); + assertArrayEquals(orig, message); + } catch (MessagingException ex) { + fail(ex.getMessage()); + } finally { + t.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + // wait for handler to exit + server.join(); + } + } + } + + @Test + public void testBdatFailure() throws Exception { + TestServer server = null; + try { + SMTPHandler handler = new SMTPHandler() { + { + { + extensions.add("CHUNKING"); + } + } + + @Override + public void bdat(String line) throws IOException { + String[] tok = line.split("\\s+"); + int bytes = Integer.parseInt(tok[1]); + boolean last = tok.length > 2 && + tok[2].equalsIgnoreCase("LAST"); + readBdatMessage(bytes, last); + println("444 failed"); + } + }; + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.smtp.chunksize", "128"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + try { + MimeMessage msg = new MimeMessage(session); + msg.setRecipients(Message.RecipientType.TO, "joe@example.com"); + msg.setSubject("test"); + msg.setText("test\r\n"); + t.connect(); + t.sendMessage(msg, msg.getAllRecipients()); + fail("no exception"); + } catch (MessagingException ex) { + // expect it to fail + } finally { + t.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + // wait for handler to exit + server.join(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPCloseTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPCloseTest.java new file mode 100644 index 0000000..f89e248 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPCloseTest.java @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import jakarta.mail.Session; +import jakarta.mail.Transport; + +import java.util.Properties; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that the socket is closed when connect times out. + */ +@Timeout(5) +public final class SMTPCloseTest { + + @Test + public void test() { + NopServer server = null; + try { + server = new NopServer(); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.smtp.timeout", "100"); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + try { + t.connect(); + } catch (Exception ex) { + // expect an exception when connect times out + } finally { + t.close(); + } + // give the server thread a chance to detect the close + for (int i = 0; i < 10 && !server.eof(); i++) + Thread.sleep(100); + assertTrue(server.eof()); + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPConnectFailureTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPConnectFailureTest.java new file mode 100644 index 0000000..1a3fdbb --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPConnectFailureTest.java @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import jakarta.mail.Session; +import jakarta.mail.Transport; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.util.MailConnectException; +import java.net.ServerSocket; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test connect failures. + */ +public class SMTPConnectFailureTest { + + private static final String HOST = "localhost"; + private static final int CTO = 20; + + @Test + public void testNoServer() { + try { + // verify that port is not being used + ServerSocket ss = new ServerSocket(0); + int port = ss.getLocalPort(); + ss.close(); + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", HOST); + properties.setProperty("mail.smtp.port", String.valueOf(port)); + properties.setProperty("mail.smtp.connectiontimeout", "" + CTO); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + fail("Connected!"); + // failure! + } catch (MailConnectException mcex) { + // success! + assertEquals(HOST, mcex.getHost()); + assertEquals(port, mcex.getPort()); + assertEquals(CTO, mcex.getConnectionTimeout()); + } catch (Exception ex) { + // expect an exception when connect times out + fail(ex.toString()); + } finally { + if (t.isConnected()) + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPHandler.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPHandler.java new file mode 100644 index 0000000..62443fd --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPHandler.java @@ -0,0 +1,284 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import org.xbib.net.mail.test.test.ProtocolHandler; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.Set; +import java.util.StringTokenizer; +import java.util.logging.Level; + +/** + * Handle connection. + * + * @author Bill Shannon + */ +public class SMTPHandler extends ProtocolHandler { + + /** + * Current line. + */ + private String currentLine; + + /** + * A message being accumulated. + */ + private ByteArrayOutputStream messageStream; + + /** + * SMTP extensions supported. + */ + protected Set extensions = new HashSet(); + + /** + * Send greetings. + * + * @throws IOException unable to write to socket + */ + @Override + public void sendGreetings() throws IOException { + println("220 localhost dummy server ready"); + } + + /** + * Send String to socket. + * + * @param str String to send + * @throws IOException unable to write to socket + */ + public void println(final String str) throws IOException { + writer.print(str); + writer.print("\r\n"); + writer.flush(); + } + + /** + * Handle command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void handleCommand() throws IOException { + currentLine = readLine(); + + if (currentLine == null) + return; + + final StringTokenizer st = new StringTokenizer(currentLine, " "); + final String commandName = st.nextToken().toUpperCase(); + if (commandName == null) { + LOGGER.severe("Command name is empty!"); + exit(); + return; + } + + if (commandName.equals("HELO")) { + helo(); + } else if (commandName.equals("EHLO")) { + ehlo(); + } else if (commandName.equals("MAIL")) { + mail(currentLine); + } else if (commandName.equals("RCPT")) { + rcpt(currentLine); + } else if (commandName.equals("DATA")) { + data(); + } else if (commandName.equals("BDAT")) { + bdat(currentLine); + } else if (commandName.equals("NOOP")) { + noop(); + } else if (commandName.equals("RSET")) { + rset(); + } else if (commandName.equals("QUIT")) { + quit(); + } else if (commandName.equals("AUTH")) { + auth(currentLine); + } else { + LOGGER.log(Level.SEVERE, "ERROR command unknown: {0}", commandName); + println("-ERR unknown command"); + } + } + + protected String readLine() throws IOException { + currentLine = super.readLine(); + + if (currentLine == null) { + // XXX - often happens when shutting down + //LOGGER.severe("Current line is null!"); + exit(); + } + return currentLine; + } + + /** + * HELO command. + * + * @throws IOException unable to read/write to socket + */ + public void helo() throws IOException { + println("220 Ok"); + } + + /** + * EHLO command. + * + * @throws IOException unable to read/write to socket + */ + public void ehlo() throws IOException { + println("250-hello"); + for (String ext : extensions) + println("250-" + ext); + println("250 AUTH PLAIN"); // PLAIN is simplest to fake + } + + /** + * MAIL command. + * + * @throws IOException unable to read/write to socket + */ + public void mail(String line) throws IOException { + ok(); + } + + /** + * RCPT command. + * + * @throws IOException unable to read/write to socket + */ + public void rcpt(String line) throws IOException { + ok(); + } + + /** + * DATA command. + * + * @throws IOException unable to read/write to socket + */ + public void data() throws IOException { + println("354 go ahead"); + readMessage(); + ok(); + } + + /** + * BDAT command. + * + * @throws IOException unable to read/write to socket + */ + public void bdat(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line, " "); + String commandName = st.nextToken(); + int bytes = Integer.parseInt(st.nextToken()); + boolean last = st.hasMoreTokens() && + st.nextToken().equalsIgnoreCase("LAST"); + readBdatMessage(bytes, last); + ok(); + } + + /** + * Allow subclasses to override to save the message. + */ + protected void setMessage(byte[] msg) { + } + + /** + * Consume the message and save it. + */ + protected void readMessage() throws IOException { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + PrintWriter pw = new PrintWriter(new OutputStreamWriter(bos, StandardCharsets.UTF_8)); + String line; + while ((line = super.readLine()) != null) { + if (line.equals(".")) + break; + if (line.startsWith(".")) + line = line.substring(1); + pw.print(line); + pw.print("\r\n"); + } + pw.close(); + setMessage(bos.toByteArray()); + } + + /** + * Consume a chunk of the message and save it. + * Save the entire message when the last chunk is received. + */ + protected void readBdatMessage(int bytes, boolean last) throws IOException { + byte[] data = new byte[bytes]; + int len = data.length; + int off = 0; + int n; + while (len > 0 && (n = in.read(data, off, len)) > 0) { + off += n; + len -= n; + } + if (messageStream == null) + messageStream = new ByteArrayOutputStream(); + messageStream.write(data); + if (last) { + setMessage(messageStream.toByteArray()); + messageStream = null; + } + } + + /** + * NOOP command. + * + * @throws IOException unable to read/write to socket + */ + public void noop() throws IOException { + ok(); + } + + /** + * RSET command. + * + * @throws IOException unable to read/write to socket + */ + public void rset() throws IOException { + ok(); + } + + /** + * QUIT command. + * + * @throws IOException unable to read/write to socket + */ + public void quit() throws IOException { + println("221 BYE"); + exit(); + } + + /** + * AUTH command. + * + * @throws IOException unable to read/write to socket + */ + public void auth(String line) throws IOException { + println("235 Authorized"); + } + + protected void ok() throws IOException { + println("250 OK"); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPIOExceptionTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPIOExceptionTest.java new file mode 100644 index 0000000..145d787 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPIOExceptionTest.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.event.ConnectionAdapter; +import jakarta.mail.event.ConnectionEvent; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.test.test.TestServer; +import java.io.IOException; +import java.util.Properties; +import java.util.concurrent.CountDownLatch; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that the connection is closed when an IOException is detected. + */ +@Timeout(5) +public final class SMTPIOExceptionTest { + + private boolean closed = false; + + private static final int TIMEOUT = 200; // I/O timeout, in millis + + @Test + public void test() throws Exception { + TestServer server = null; + final CountDownLatch closedLatch = new CountDownLatch(1); + try { + SMTPHandler handler = new SMTPHandler() { + @Override + public void rcpt(String line) throws IOException { + try { + // delay long enough to cause timeout + Thread.sleep(2 * TIMEOUT); + } catch (Exception ex) { + } + super.rcpt(line); + } + }; + server = new TestServer(handler); + server.start(); + + final Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.smtp.timeout", "" + TIMEOUT); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + /* + * Use a listener to detect the connection being closed + * because if we called isConnected() and the connection + * wasn't already closed, it will issue a command that + * might detect that the connection was closed, even + * though it wasn't closed already. + */ + t.addConnectionListener(new ConnectionAdapter() { + @Override + public void closed(ConnectionEvent e) { + closedLatch.countDown(); + } + }); + try { + MimeMessage msg = new MimeMessage(session); + msg.setRecipients(Message.RecipientType.TO, "joe@example.com"); + msg.setSubject("test"); + msg.setText("test"); + t.connect(); + t.sendMessage(msg, msg.getAllRecipients()); + } catch (MessagingException ex) { + // expect an exception from sendMessage + closedLatch.await(); // wait for the listener to run + // if we get here, the listener was called - SUCCESS + } finally { + t.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + // wait for handler to exit + server.join(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPLoginHandler.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPLoginHandler.java new file mode 100644 index 0000000..3924e05 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPLoginHandler.java @@ -0,0 +1,145 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.StringTokenizer; +import java.util.logging.Level; + +/** + * Handle connection with LOGIN or PLAIN authentication. + * + * @author Bill Shannon + */ +public class SMTPLoginHandler extends SMTPHandler { + protected String username = "test"; + protected String password = "test"; + + /** + * EHLO command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void ehlo() throws IOException { + println("250-hello"); + println("250-SMTPUTF8"); + println("250-8BITMIME"); + println("250 AUTH PLAIN LOGIN"); + } + + /** + * AUTH command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void auth(String line) throws IOException { + StringTokenizer ct = new StringTokenizer(line, " "); + String commandName = ct.nextToken().toUpperCase(); + String mech = ct.nextToken().toUpperCase(); + String ir = ""; + if (ct.hasMoreTokens()) + ir = ct.nextToken(); + + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine(line); + if (mech.equalsIgnoreCase("PLAIN")) + plain(ir); + else if (mech.equalsIgnoreCase("LOGIN")) + login(ir); + else + println("501 bad AUTH mechanism"); + } + + /** + * AUTH LOGIN + */ + private void login(String ir) throws IOException { + println("334"); + // read user name + String resp = readLine(); + if (!isBase64(resp)) { + println("501 response not base64"); + return; + } + byte[] response = resp.getBytes(StandardCharsets.US_ASCII); + response = Base64.getDecoder().decode(response); + String u = new String(response, StandardCharsets.UTF_8); + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("USER: " + u); + println("334"); + + // read password + resp = readLine(); + if (!isBase64(resp)) { + println("501 response not base64"); + return; + } + response = resp.getBytes(StandardCharsets.US_ASCII); + response = Base64.getDecoder().decode(response); + String p = new String(response, StandardCharsets.UTF_8); + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("PASSWORD: " + p); + + //System.out.printf("USER: %s, PASSWORD: %s%n", u, p); + if (!u.equals(username) || !p.equals(password)) { + println("535 authentication failed"); + return; + } + + println("235 Authenticated"); + } + + /** + * AUTH PLAIN + */ + private void plain(String ir) throws IOException { + String auth = new String(Base64.getDecoder().decode( + ir.getBytes(StandardCharsets.US_ASCII)), + StandardCharsets.UTF_8); + String[] ap = auth.split("\000"); + String u = ap[1]; + String p = ap[2]; + //System.out.printf("USER: %s, PASSWORD: %s%n", u, p); + if (!u.equals(username) || !p.equals(password)) { + println("535 authentication failed"); + return; + } + println("235 Authenticated"); + } + + /** + * Is every character in the string a base64 character? + */ + private boolean isBase64(String s) { + int len = s.length(); + if (s.endsWith("==")) + len -= 2; + else if (s.endsWith("=")) + len--; + for (int i = 0; i < len; i++) { + char c = s.charAt(i); + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '+' || c == '/')) + return false; + } + return true; + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPSaslHandler.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPSaslHandler.java new file mode 100644 index 0000000..2cfe929 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPSaslHandler.java @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import org.xbib.net.mail.util.ASCIIUtility; + +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.sasl.AuthorizeCallback; +import javax.security.sasl.RealmCallback; +import javax.security.sasl.RealmChoiceCallback; +import javax.security.sasl.Sasl; +import javax.security.sasl.SaslException; +import javax.security.sasl.SaslServer; +import java.io.IOException; +import java.util.Base64; +import java.util.StringTokenizer; +import java.util.logging.Level; + +/** + * Handle connection with SASL authentication. + * + * @author Bill Shannon + */ +public class SMTPSaslHandler extends SMTPHandler { + + /** + * EHLO command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void ehlo() throws IOException { + println("250-hello"); + println("250 AUTH DIGEST-MD5"); + } + + /** + * AUTH command. + * + * @throws IOException unable to read/write to socket + */ + @Override + public void auth(String line) throws IOException { + StringTokenizer ct = new StringTokenizer(line, " "); + String commandName = ct.nextToken().toUpperCase(); + String mech = ct.nextToken().toUpperCase(); + String ir = ""; + if (ct.hasMoreTokens()) + ir = ct.nextToken(); + + final String u = "test"; + final String p = "test"; + final String realm = "test"; + + CallbackHandler cbh = new CallbackHandler() { + @Override + public void handle(Callback[] callbacks) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL callback length: " + callbacks.length); + for (int i = 0; i < callbacks.length; i++) { + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL callback " + i + ": " + callbacks[i]); + if (callbacks[i] instanceof NameCallback) { + NameCallback ncb = (NameCallback) callbacks[i]; + ncb.setName(u); + } else if (callbacks[i] instanceof PasswordCallback) { + PasswordCallback pcb = (PasswordCallback) callbacks[i]; + pcb.setPassword(p.toCharArray()); + } else if (callbacks[i] instanceof AuthorizeCallback) { + AuthorizeCallback ac = (AuthorizeCallback) callbacks[i]; + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL authorize: " + + "authn: " + ac.getAuthenticationID() + ", " + + "authz: " + ac.getAuthorizationID() + ", " + + "authorized: " + ac.getAuthorizedID()); + ac.setAuthorized(true); + } else if (callbacks[i] instanceof RealmCallback) { + RealmCallback rcb = (RealmCallback) callbacks[i]; + rcb.setText(realm != null ? + realm : rcb.getDefaultText()); + } else if (callbacks[i] instanceof RealmChoiceCallback) { + RealmChoiceCallback rcb = + (RealmChoiceCallback) callbacks[i]; + if (realm == null) + rcb.setSelectedIndex(rcb.getDefaultChoice()); + else { + // need to find specified realm in list + String[] choices = rcb.getChoices(); + for (int k = 0; k < choices.length; k++) { + if (choices[k].equals(realm)) { + rcb.setSelectedIndex(k); + break; + } + } + } + } + } + } + }; + + SaslServer ss; + try { + ss = Sasl.createSaslServer(mech, "smtp", "localhost", null, cbh); + } catch (SaslException sex) { + LOGGER.log(Level.FINE, "Failed to create SASL server", sex); + println("501 Failed to create SASL server"); + return; + } + if (ss == null) { + LOGGER.fine("No SASL support"); + println("501 No SASL support"); + return; + } + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL server " + ss.getMechanismName()); + + byte[] response = ir.getBytes(); + while (!ss.isComplete()) { + try { + byte[] chal = ss.evaluateResponse(response); + // send challenge + if (LOGGER.isLoggable(Level.FINE)) + LOGGER.fine("SASL challenge: " + + ASCIIUtility.toString(chal, 0, chal.length)); + byte[] ba = Base64.getEncoder().encode(chal); + if (ba.length > 0) + println("334 " + + ASCIIUtility.toString(ba, 0, ba.length)); + else + println("334"); + // read response + String resp = readLine(); + if (!isBase64(resp)) { + println("501 response not base64"); + break; + } + response = resp.getBytes(); + response = Base64.getDecoder().decode(response); + } catch (SaslException ex) { + println("501 " + ex.toString()); + break; + } + } + + if (ss.isComplete() /*&& status == SUCCESS*/) { + String qop = (String) ss.getNegotiatedProperty(Sasl.QOP); + if (qop != null && (qop.equalsIgnoreCase("auth-int") || + qop.equalsIgnoreCase("auth-conf"))) { + // XXX - NOT SUPPORTED!!! + LOGGER.fine( + "SASL Mechanism requires integrity or confidentiality"); + println("501 " + + "SASL Mechanism requires integrity or confidentiality"); + return; + } + } + + println("235 Authenticated"); + } + + /** + * Is every character in the string a base64 character? + */ + private boolean isBase64(String s) { + int len = s.length(); + if (s.endsWith("==")) + len -= 2; + else if (s.endsWith("=")) + len--; + for (int i = 0; i < len; i++) { + char c = s.charAt(i); + if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || + (c >= '0' && c <= '9') || c == '+' || c == '/')) + return false; + } + return true; + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPSaslLoginTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPSaslLoginTest.java new file mode 100644 index 0000000..d204d67 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPSaslLoginTest.java @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import jakarta.mail.AuthenticationFailedException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.TestServer; +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test login using a SASL mechanism on the server + * with SASL and non-SASL on the client. + */ +public class SMTPSaslLoginTest { + + /** + * Test using non-SASL DIGEST-MD5. + */ + @Test + public void testSuccess() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler()); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test using non-SASL DIGEST-MD5 with incorrect password. + */ + @Test + public void testFailure() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler()); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "xtest"); + // should have failed + fail("wrong password succeeded"); + } catch (AuthenticationFailedException ex) { + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test using SASL DIGEST-MD5. + */ + @Test + public void testSaslSuccess() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler()); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.smtp.sasl.enable", "true"); + properties.setProperty("mail.smtp.sasl.mechanisms", "DIGEST-MD5"); + properties.setProperty("mail.smtp.auth.digest-md5.disable", "true"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test using SASL DIGEST-MD5 with incorrect password. + */ + @Test + public void testSaslFailure() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler()); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.smtp.sasl.enable", "true"); + properties.setProperty("mail.smtp.sasl.mechanisms", "DIGEST-MD5"); + properties.setProperty("mail.smtp.auth.digest-md5.disable", "true"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "xtest"); + // should have failed + fail("wrong password succeeded"); + } catch (AuthenticationFailedException ex) { + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test that AUTH with no mechanisms fails. + */ + @Test + public void testAuthNoParam() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler() { + @Override + public void ehlo() throws IOException { + println("250-hello"); + println("250 AUTH"); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + fail("Connect didn't fail"); + } catch (AuthenticationFailedException ex) { + // success + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test that no AUTH succeeds by skipping authentication entirely. + */ + @Test + public void testNoAuth() { + TestServer server = null; + try { + server = new TestServer(new SMTPSaslHandler() { + @Override + public void ehlo() throws IOException { + println("250-hello"); + println("250 XXX"); + } + + @Override + public void auth(String line) throws IOException { + println("501 Authentication failed"); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect("test", "test"); + // success + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPUknownCodeTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPUknownCodeTest.java new file mode 100644 index 0000000..6dcf3e0 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPUknownCodeTest.java @@ -0,0 +1,83 @@ +/* + * Copyright (c) 2022 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 org.xbib.net.mail.test.smtp; + +import jakarta.mail.Message; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.TestServer; + +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.fail; + +public class SMTPUknownCodeTest { + + /** + * Test handling of unknown 2xx status code from the server + */ + @Test + public void testUnknown2xy() { + TestServer server = null; + try { + server = new TestServer(new SMTPLoginHandler() { + @Override + public void rcpt(String line) throws IOException { + if (line.contains("alex")) { + println("254 XY"); + } else { + super.rcpt(line); + } + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.smtp.auth.mechanisms", "LOGIN"); +// properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); +// session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + MimeMessage msg = new MimeMessage(session); + msg.setRecipients(Message.RecipientType.TO, "joe@example.com"); + msg.addRecipients(Message.RecipientType.TO, "alex@example.com"); + msg.setSubject("test"); + msg.setText("test"); + t.connect(); + t.sendMessage(msg, msg.getAllRecipients()); + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPUtf8Test.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPUtf8Test.java new file mode 100644 index 0000000..2ac28d6 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPUtf8Test.java @@ -0,0 +1,260 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import jakarta.mail.Message; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.TestServer; +import java.io.IOException; +import java.util.Properties; +import java.util.StringTokenizer; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test UTF-8 user names and recipients. + */ +public class SMTPUtf8Test { + + /** + * Test using UTF-8 user name. + */ + @Test + public void testUtf8UserName() { + TestServer server = null; + final String user = "test\u03b1"; + try { + server = new TestServer(new SMTPLoginHandler() { + @Override + public void auth(String line) throws IOException { + username = user; + password = user; + super.auth(line); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.smtp.auth.mechanisms", "LOGIN"); + properties.setProperty("mail.mime.allowutf8", "true"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect(user, user); + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test using UTF-8 user name but without mail.mime.allowutf8. + */ + @Test + public void testUtf8UserNameNoAllowUtf8() { + TestServer server = null; + final String user = "test\u03b1"; + try { + server = new TestServer(new SMTPLoginHandler() { + @Override + public void auth(String line) throws IOException { + username = user; + password = user; + super.auth(line); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.smtp.auth.mechanisms", "LOGIN"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect(user, user); + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + /** + * Test using UTF-8 user name and PLAIN but without mail.mime.allowutf8. + */ + @Test + public void testUtf8UserNamePlain() { + TestServer server = null; + final String user = "test\u03b1"; + try { + server = new TestServer(new SMTPLoginHandler() { + @Override + public void auth(String line) throws IOException { + username = user; + password = user; + super.auth(line); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.smtp.auth.mechanisms", "PLAIN"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + t.connect(user, user); + // success! + } catch (Exception ex) { + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + } + + private static class Envelope { + public String from; + public String to; + } + + /** + * Test using UTF-8 From and To address. + */ + @Test + public void testUtf8From() { + TestServer server = null; + final String test = "test\u03b1"; + final String saddr = test + "@" + test + ".com"; + final Envelope env = new Envelope(); + try { + server = new TestServer(new SMTPHandler() { + @Override + public void ehlo() throws IOException { + println("250-hello"); + println("250-SMTPUTF8"); + println("250 AUTH PLAIN"); + } + + @Override + public void mail(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip "MAIL" + env.from = st.nextToken(). + replaceFirst("FROM:<(.*)>", "$1"); + if (!st.hasMoreTokens() || + !st.nextToken().equals("SMTPUTF8")) + println("500 fail"); + else + ok(); + } + + @Override + public void rcpt(String line) throws IOException { + StringTokenizer st = new StringTokenizer(line); + st.nextToken(); // skip "RCPT" + env.to = st.nextToken(). + replaceFirst("TO:<(.*)>", "$1"); + ok(); + } + }); + server.start(); + + Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.mime.allowutf8", "true"); + //properties.setProperty("mail.debug.auth", "true"); + Session session = Session.getInstance(properties); + //session.setDebug(true); + + Transport t = session.getTransport("smtp"); + try { + MimeMessage msg = new MimeMessage(session); + InternetAddress addr = new InternetAddress(saddr, test); + msg.setFrom(addr); + msg.setRecipient(Message.RecipientType.TO, addr); + msg.setSubject(test); + msg.setText(test + "\n"); + t.connect("test", "test"); + t.sendMessage(msg, msg.getAllRecipients()); + } catch (Exception ex) { + ex.printStackTrace(); + fail(ex.toString()); + } finally { + t.close(); + } + } catch (Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + } + } + // after we're sure the server is done + assertEquals(saddr, env.from); + assertEquals(saddr, env.to); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPWriteTimeoutTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPWriteTimeoutTest.java new file mode 100644 index 0000000..52d8edf --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/smtp/SMTPWriteTimeoutTest.java @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.smtp; + +import jakarta.activation.DataHandler; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Transport; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.util.ByteArrayDataSource; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.test.test.TestServer; +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that write timeouts work. + */ +@Timeout(10) +public final class SMTPWriteTimeoutTest { + + + private static final int TIMEOUT = 200; // write timeout, in millis + + @Test + public void test() throws Exception { + TestServer server = null; + try { + SMTPHandler handler = new SMTPHandler() { + @Override + public void readMessage() throws IOException { + try { + // delay long enough to cause timeout + Thread.sleep(5 * TIMEOUT); + } catch (Exception ex) { + } + super.readMessage(); + } + }; + server = new TestServer(handler); + server.start(); + Thread.sleep(1000); + + final Properties properties = new Properties(); + properties.setProperty("mail.smtp.host", "localhost"); + properties.setProperty("mail.smtp.port", String.valueOf(server.getPort())); + properties.setProperty("mail.smtp.writetimeout", "" + TIMEOUT); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + final Transport t = session.getTransport("smtp"); + try { + MimeMessage msg = new MimeMessage(session); + msg.setRecipients(Message.RecipientType.TO, "joe@example.com"); + msg.setSubject("test"); + byte[] bytes = new byte[16 * 1024 * 1024]; + msg.setDataHandler( + new DataHandler(new ByteArrayDataSource(bytes, + "application/octet-stream"))); + t.connect(); + t.sendMessage(msg, msg.getAllRecipients()); + fail("No exception"); + } catch (MessagingException ex) { + // expect an exception from sendMessage + } finally { + t.close(); + } + } catch (final Exception e) { + e.printStackTrace(); + fail(e.getMessage()); + } finally { + if (server != null) { + server.quit(); + server.interrupt(); + // wait long enough for handler to exit + Thread.sleep(2 * TIMEOUT); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamTest.java new file mode 100644 index 0000000..8f22ab7 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamTest.java @@ -0,0 +1,124 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.stream; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.util.LineInputStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; + +/** + * Test handling of line terminators. + * LineInputStream handles these different line terminators: + * + * NL - Unix + * CR LF - Windows, MIME + * CR - old MacOS + * CR CR LF - broken internet servers + * + * @author Bill Shannon + */ + +public class LineInputStreamTest { + private static final String[] lines = { + "line1\nline2\nline3\n", + "line1\r\nline2\r\nline3\r\n", + "line1\rline2\rline3\r", + "line1\r\r\nline2\r\r\nline3\r\r\n" + }; + + private static final String[] empty = { + "\n\n\n", + "\r\n\r\n\r\n", + "\r\r\r", + "\r\r\n\r\r\n\r\r\n" + }; + + private static final String[] mixed = { + "line1\n\nline3\n", + "line1\r\n\r\nline3\r\n", + "line1\r\rline3\r", + "line1\r\r\n\r\r\nline3\r\r\n" + }; + + @Test + public void testLines() throws IOException { + for (String s : lines) { + LineInputStream is = createStream(s); + assertEquals("line1", is.readLine()); + assertEquals("line2", is.readLine()); + assertEquals("line3", is.readLine()); + assertEquals(null, is.readLine()); + } + } + + @Test + public void testEmpty() throws IOException { + for (String s : empty) { + LineInputStream is = createStream(s); + assertEquals("", is.readLine()); + assertEquals("", is.readLine()); + assertEquals("", is.readLine()); + assertEquals(null, is.readLine()); + } + } + + @Test + public void testMixed() throws IOException { + for (String s : mixed) { + LineInputStream is = createStream(s); + assertEquals("line1", is.readLine()); + assertEquals("", is.readLine()); + assertEquals("line3", is.readLine()); + assertEquals(null, is.readLine()); + } + } + + @Test + public void testUtf8Fail() throws IOException { + LineInputStream is = createStream("a\u00A9b\n", StandardCharsets.UTF_8); + assertNotEquals("a\u00A9b", is.readLine()); + } + + @Test + public void testUtf8() throws IOException { + LineInputStream is = new LineInputStream(new ByteArrayInputStream( + "a\u00A9b\n".getBytes(StandardCharsets.UTF_8)), true); + assertEquals("a\u00A9b", is.readLine()); + } + + @Test + public void testIso() throws IOException { + LineInputStream is = + createStream("a\251b\n", StandardCharsets.ISO_8859_1); + assertEquals("a\251b", is.readLine()); + } + + private LineInputStream createStream(String s) { + return createStream(s, StandardCharsets.US_ASCII); + } + + private LineInputStream createStream(String s, Charset cs) { + return new LineInputStream( + new ByteArrayInputStream(s.getBytes(cs))); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamUtf8FailTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamUtf8FailTest.java new file mode 100644 index 0000000..a216f9c --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamUtf8FailTest.java @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.stream; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.util.LineInputStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test that the "mail.mime.allowutf8" System property + * not set doesn't allow UTF-8 data to be read. + */ +public class LineInputStreamUtf8FailTest { + + @BeforeAll + public static void before() { + System.out.println("LineInputStreamUtf8Fail"); + System.clearProperty("mail.mime.allowutf8"); + } + + @Test + public void testUtf8() throws Exception { + LineInputStream is = new LineInputStream(new ByteArrayInputStream( + "a\u00A9b\n".getBytes(StandardCharsets.UTF_8)), false); + assertEquals("a\302\251b", is.readLine()); + } + + @Test + public void testIso() throws IOException { + LineInputStream is = new LineInputStream(new ByteArrayInputStream( + "a\251b\n".getBytes(StandardCharsets.ISO_8859_1)), false); + assertEquals("a\251b", is.readLine()); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamUtf8Test.java b/net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamUtf8Test.java new file mode 100644 index 0000000..d73e28a --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/stream/LineInputStreamUtf8Test.java @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.stream; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.util.LineInputStream; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test that the "mail.mime.allowutf8" System property + * set to true allows UTF-8 data to be read. + */ +public class LineInputStreamUtf8Test { + + @BeforeAll + public static void before() { + System.out.println("LineInputStreamUtf8"); + System.setProperty("mail.mime.allowutf8", "true"); + } + + @Test + public void testUtf8() throws Exception { + LineInputStream is = new LineInputStream(new ByteArrayInputStream( + "a\u00A9b\n".getBytes(StandardCharsets.UTF_8)), false); + assertEquals("a\u00A9b", is.readLine()); + } + + @Test + public void testIso() throws IOException { + LineInputStream is = new LineInputStream(new ByteArrayInputStream( + "a\251b\n".getBytes(StandardCharsets.ISO_8859_1)), false); + assertEquals("a\251b", is.readLine()); + } + + @AfterAll + public static void after() { + System.clearProperty("mail.mime.allowutf8"); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/test/AsciiStringInputStream.java b/net-mail/src/test/java/org/xbib/net/mail/test/test/AsciiStringInputStream.java new file mode 100644 index 0000000..8e16f6a --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/test/AsciiStringInputStream.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2015, 2023 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 org.xbib.net.mail.test.test; + +import java.io.InputStream; + +/** + * Replacement for deprecated java.io.StringBufferInputStream + */ +public class AsciiStringInputStream extends InputStream { + + private final String input; + private int position; + + public AsciiStringInputStream(String input) { + this(input, true); + } + + public AsciiStringInputStream(String input, boolean strict) { + if (strict) { + for (int i = 0; i < input.length(); i++) { + if (input.charAt(i) > 0x7F) { + throw new IllegalArgumentException("Not an ASCII string"); + } + } + } + + this.input = input; + } + + @Override + public int read() { + if (position < input.length()) { + return input.charAt(position++) & 0xFF; + } else { + return -1; + } + } + +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/test/NullOutputStream.java b/net-mail/src/test/java/org/xbib/net/mail/test/test/NullOutputStream.java new file mode 100644 index 0000000..873adc4 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/test/NullOutputStream.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.test; + +import java.io.IOException; +import java.io.OutputStream; + +/** + * An OutputStream that throws away all data written to it. + */ +public class NullOutputStream extends OutputStream { + + @Override + public void write(int b) throws IOException { + } + + @Override + public void write(byte[] b) throws IOException { + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/test/ProtocolHandler.java b/net-mail/src/test/java/org/xbib/net/mail/test/test/ProtocolHandler.java new file mode 100644 index 0000000..bc6a931 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/test/ProtocolHandler.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.test; + +import javax.net.ssl.SSLException; +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.io.PushbackInputStream; +import java.net.Socket; +import java.net.SocketException; +import java.nio.charset.StandardCharsets; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Handle protocol connection. + * + * Inspired by, and derived from, POP3Handler by sbo. + * + * @author sbo + * @author Bill Shannon + */ +public abstract class ProtocolHandler implements Runnable, Cloneable { + + /** + * Logger for this class. + */ + protected final Logger LOGGER = Logger.getLogger(this.getClass().getName()); + + /** + * Client socket. + */ + protected Socket clientSocket; + + /** + * Quit? + */ + protected boolean quit; + + /** + * Writer to socket. + */ + protected PrintWriter writer; + + /** + * Input from socket. + */ + protected InputStream in; + + /** + * Sets the client socket. + * + * @param clientSocket the client socket + */ + public final void setClientSocket(final Socket clientSocket) + throws IOException { + this.clientSocket = clientSocket; + writer = new PrintWriter(new OutputStreamWriter( + clientSocket.getOutputStream(), StandardCharsets.UTF_8)); + in = new BufferedInputStream(clientSocket.getInputStream()); + } + + /** + * Optionally send a greeting when first connected. + */ + public void sendGreetings() throws IOException { + } + + /** + * Read and process a single command. + */ + public abstract void handleCommand() throws IOException; + + /** + * Read a single line terminated by newline or CRLF. + * Convert the UTF-8 bytes in the line (minus the line terminator) + * to a String. + */ + protected String readLine() throws IOException { + byte[] buf = new byte[128]; + + int room = buf.length; + int offset = 0; + int c; + + while ((c = in.read()) != -1) { + if (c == '\n') { + break; + } else if (c == '\r') { + int c2 = in.read(); + if ((c2 != '\n') && (c2 != -1)) { + if (!(in instanceof PushbackInputStream)) + this.in = new PushbackInputStream(in); + ((PushbackInputStream) in).unread(c2); + } + break; + } else { + if (--room < 0) { + byte[] nbuf = new byte[offset + 128]; + room = nbuf.length - offset - 1; + System.arraycopy(buf, 0, nbuf, 0, offset); + buf = nbuf; + } + buf[offset++] = (byte) c; + } + } + if ((c == -1) && (offset == 0)) + return null; + return new String(buf, 0, offset, StandardCharsets.UTF_8); + } + + /** + * {@inheritDoc} + */ + @Override + public final void run() { + try { + + sendGreetings(); + + while (!quit) { + handleCommand(); + } + + //clientSocket.close(); + } catch (SocketException sex) { + // ignore it, often get "connection reset" when client closes + } catch (SSLException sex) { + // ignore it, often occurs when testing SSL + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Error", e); + } finally { + try { + if (clientSocket != null) + clientSocket.close(); + } catch (final IOException ioe) { + LOGGER.log(Level.SEVERE, "Error", ioe); + } + } + } + + /** + * Quit. + */ + public void exit() { + quit = true; + try { + if (clientSocket != null && !clientSocket.isClosed()) { + clientSocket.close(); + clientSocket = null; + } + } catch (final IOException e) { + LOGGER.log(Level.SEVERE, "Error", e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public Object clone() { + try { + return super.clone(); + } catch (final CloneNotSupportedException e) { + throw new AssertionError(e); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/test/ReflectionUtil.java b/net-mail/src/test/java/org/xbib/net/mail/test/test/ReflectionUtil.java new file mode 100644 index 0000000..29ec972 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/test/ReflectionUtil.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2022 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 org.xbib.net.mail.test.test; + +import java.lang.reflect.Field; + +public final class ReflectionUtil { + private ReflectionUtil() { + throw new UnsupportedOperationException(); + } + + public static Field setFieldValue(Object object, + String fieldName, + Object valueTobeSet) throws NoSuchFieldException, IllegalAccessException { + Field field = getField(object.getClass(), fieldName); + field.setAccessible(true); + field.set(object, valueTobeSet); + return field; + } + + public static Object getPrivateFieldValue(Object object, + String fieldName) throws NoSuchFieldException, IllegalAccessException { + Field field = getField(object.getClass(), fieldName); + field.setAccessible(true); + return field.get(object); + } + + private static Field getField(Class mClass, String fieldName) throws NoSuchFieldException { + try { + return mClass.getDeclaredField(fieldName); + } catch (NoSuchFieldException e) { + Class superClass = mClass.getSuperclass(); + if (superClass == null) { + throw e; + } else { + return getField(superClass, fieldName); + } + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/test/SavedSocketFactory.java b/net-mail/src/test/java/org/xbib/net/mail/test/test/SavedSocketFactory.java new file mode 100644 index 0000000..16f7178 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/test/SavedSocketFactory.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2012, 2023 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 org.xbib.net.mail.test.test; + +import javax.net.SocketFactory; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; + +/** + * A SocketFactory that saves the Socket it creates so that it can be + * accessed later. Useful for checking that sockets are closed properly. + */ +public class SavedSocketFactory extends SocketFactory { + private SocketFactory factory; + private Socket saved; + + public SavedSocketFactory() { + super(); + try { + factory = SocketFactory.getDefault(); + } catch (Exception ex) { + // ignore + } + } + + @Override + public Socket createSocket() throws IOException { + return save(factory.createSocket()); + } + + @Override + public Socket createSocket(InetAddress host, int port) + throws IOException { + return save(factory.createSocket(host, port)); + } + + @Override + public Socket createSocket(InetAddress address, int port, + InetAddress localAddress, int localPort) + throws IOException { + return save(factory.createSocket( + address, port, localAddress, localPort)); + } + + @Override + public Socket createSocket(String host, int port) throws IOException { + return save(factory.createSocket(host, port)); + } + + @Override + public Socket createSocket(String host, int port, + InetAddress localHost, int localPort) + throws IOException { + return save(factory.createSocket(host, port, localHost, localPort)); + } + + public Socket getSocket() { + return saved; + } + + private Socket save(Socket s) { + saved = s; + return saved; + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/test/SessionTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/test/SessionTest.java new file mode 100644 index 0000000..5c5d557 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/test/SessionTest.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2021 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 org.xbib.net.mail.test.test; + +import jakarta.mail.Provider; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.imap.IMAPProvider; + +import java.lang.annotation.Annotation; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SessionTest { + + private static final String DEFAULT_PROVIDER = "org.xbib.net.mail.util.DefaultProvider"; + + @Test + public void defaultProvider() { + assertTrue(containsDefaultProvider(new IMAPProvider())); + assertFalse(containsDefaultProvider(new Provider(Provider.Type.STORE, "imap", Object.class.getName(), "xbib", null) { + })); + } + + private boolean containsDefaultProvider(Provider provider) { + Annotation[] annotations = provider.getClass().getDeclaredAnnotations(); + for (Annotation annotation : annotations) { + if (DEFAULT_PROVIDER.equals(annotation.annotationType().getName())) { + return true; + } + } + return false; + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/test/TestSSLSocketFactory.java b/net-mail/src/test/java/org/xbib/net/mail/test/test/TestSSLSocketFactory.java new file mode 100644 index 0000000..58d6648 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/test/TestSSLSocketFactory.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.test; + +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import org.xbib.net.mail.util.MailSSLSocketFactory; + +import javax.net.ssl.SSLSocket; +import javax.net.ssl.SSLSocketFactory; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; +import java.security.NoSuchAlgorithmException; + +/** + * An SSL socket factory for testing that tracks whether it's being used. + * Always trusts the server "localhost". + *

+ * + * An instance of this factory can be set as the value of the + * mail.<protocol>.ssl.socketFactory property. + * + * @since JavaMail 1.5.3 + * @author Stephan Sann + * @author Bill Shannon + */ +public class TestSSLSocketFactory extends SSLSocketFactory { + + /** + * Holds a SSLSocketFactory to pass all API-method-calls to + */ + private SSLSocketFactory defaultFactory = null; + + /** + * Was a socket created? + */ + private boolean socketCreated; + + /** + * Was a socket wrapped? + */ + private boolean socketWrapped; + + private String[] suites; + + /** + * Initializes a new TestSSLSocketFactory. + * + */ + public TestSSLSocketFactory() throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + this("TLS"); + } + + /** + * Initializes a new TestSSLSocketFactory 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 + */ + public TestSSLSocketFactory(String protocol) throws NoSuchAlgorithmException, KeyStoreException, KeyManagementException { + // Get the default SSLSocketFactory to delegate all API-calls to. + // Use a MailSSLSocketFactory so that we can trust "localhost". + defaultFactory = new MailSSLSocketFactory(); + ((MailSSLSocketFactory) defaultFactory).setTrustedHosts("localhost"); + } + + /** + * Was a socket created using one of the createSocket methods? + */ + public boolean getSocketCreated() { + return socketCreated; + } + + /** + * Was a socket wrapped using the createSocket method that takes a Socket? + */ + public boolean getSocketWrapped() { + return socketWrapped; + } + + /** + * Set the default cipher suites to be applied to future sockets. + */ + public void setDefaultCipherSuites(String[] suites) { + this.suites = suites; + } + + /** + * Configure the socket to be returned. + */ + private Socket configure(Socket socket) { + if (socket instanceof SSLSocket) { // XXX - always true + SSLSocket s = (SSLSocket) socket; + if (suites != null) + s.setEnabledCipherSuites(suites); + } + return socket; + } + + + // 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 { + Socket wrappedSocket = defaultFactory.createSocket(socket, s, i, flag); + socketWrapped = true; + return configure(wrappedSocket); + } + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#getDefaultCipherSuites() + */ + @Override + public synchronized String[] getDefaultCipherSuites() { + if (suites != null) + return suites.clone(); + else + return defaultFactory.getDefaultCipherSuites(); + } + + /* (non-Javadoc) + * @see javax.net.ssl.SSLSocketFactory#getSupportedCipherSuites() + */ + @Override + public synchronized String[] getSupportedCipherSuites() { + return defaultFactory.getSupportedCipherSuites(); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket() + */ + @Override + public synchronized Socket createSocket() throws IOException { + Socket socket = defaultFactory.createSocket(); + socketCreated = true; + return configure(socket); + } + + /* (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 { + Socket socket = + defaultFactory.createSocket(inetaddress, i, inetaddress1, j); + socketCreated = true; + return configure(socket); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i) + throws IOException { + Socket socket = defaultFactory.createSocket(inetaddress, i); + socketCreated = true; + return configure(socket); + } + + /* (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 { + Socket socket = defaultFactory.createSocket(s, i, inetaddress, j); + socketCreated = true; + return configure(socket); + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int) + */ + @Override + public synchronized Socket createSocket(String s, int i) + throws IOException, UnknownHostException { + Socket socket = defaultFactory.createSocket(s, i); + socketCreated = true; + return configure(socket); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/test/TestServer.java b/net-mail/src/test/java/org/xbib/net/mail/test/test/TestServer.java new file mode 100644 index 0000000..7c070e4 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/test/TestServer.java @@ -0,0 +1,268 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.test; + +import java.security.KeyManagementException; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLServerSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.security.KeyStore; +import java.util.ArrayList; +import java.util.List; + +/** + * A simple server for testing. + * + * Inspired by, and derived from, POP3Server by sbo. + * + * For SSL/TLS support, depends on a keystore with a single X509 certificate in + * mail/src/test/resources/com/sun/mail/test/keystore.jks. + * + * @author sbo + * @author Bill Shannon + */ +public final class TestServer extends Thread { + + /** + * Server socket. + */ + private ServerSocket serverSocket; + + /** + * Keep on? + */ + private volatile boolean keepOn; + + /** + * Protocol handler. + */ + private final ProtocolHandler handler; + + private List clients = new ArrayList(); + + /** + * Test server. + * + * @param handler the protocol handler + */ + public TestServer(final ProtocolHandler handler) throws IOException { + this(handler, false); + } + + /** + * Test server. + * + * @param handler the protocol handler + * @param isSSL create SSL sockets? + */ + public TestServer(final ProtocolHandler handler, final boolean isSSL) + throws IOException { + this.handler = handler; + + /* + * Allowing the JDK to pick a port number sometimes results in it + * picking a number that's already in use by another process, but + * no error is returned. Picking it ourself allows us to make sure + * that it's not used before we pick it. Hopefully the socket + * creation will fail if the port is already in use. + * + * XXX - perhaps we should use Random to choose a port number in + * the emphemeral range, in case a lot of low port numbers are + * already in use. + */ + for (int port = 49152; port < 50000 /*65535*/; port++) { + /* + if (isListening(port)) + continue; + */ + try { + serverSocket = createServerSocket(port, isSSL); + return; + } catch (IOException ex) { + // ignore + } catch (UnrecoverableKeyException | CertificateException | KeyStoreException | NoSuchAlgorithmException | + KeyManagementException e) { + throw new RuntimeException(e); + } + } + throw new RuntimeException("Can't find unused port"); + } + + private static ServerSocket createServerSocket(int port, boolean isSSL) + throws IOException, UnrecoverableKeyException, CertificateException, KeyStoreException, NoSuchAlgorithmException, KeyManagementException { + ServerSocket ss; + if (isSSL) { + SSLContext sslContext = createSSLContext(); + SSLServerSocketFactory sf = sslContext.getServerSocketFactory(); + ss = sf.createServerSocket(port); + } else + ss = new ServerSocket(port); + return ss; + } + + private static SSLContext createSSLContext() + throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException, UnrecoverableKeyException, KeyManagementException { + KeyStore keyStore = KeyStore.getInstance("JKS"); + keyStore.load(TestServer.class.getResourceAsStream("keystore.jks"), + "changeit".toCharArray()); + + // Create key manager + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(keyStore, "changeit".toCharArray()); + KeyManager[] km = kmf.getKeyManagers(); + + // Create trust manager + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(keyStore); + TrustManager[] tm = tmf.getTrustManagers(); + + // Initialize SSLContext + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(km, tm, null); + + return sslContext; + } + + /** + * Return the port the server is listening on. + */ + public int getPort() { + return serverSocket.getLocalPort(); + } + + /** + * Exit server. + */ + public void quit() { + try { + keepOn = false; + if (serverSocket != null && !serverSocket.isClosed()) { + serverSocket.close(); + serverSocket = null; + } + } catch (final IOException e) { + throw new RuntimeException(e); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void start() { + super.start(); + // don't return until server is really listening + // XXX - this might not be necessary + for (int tries = 0; tries < 10; tries++) { + if (isListening(getPort())) { + return; + } + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + } + } + throw new RuntimeException("Server isn't listening"); + } + + /** + * {@inheritDoc} + */ + @Override + public void run() { + try { + keepOn = true; + + while (keepOn) { + try { + final Socket clientSocket = serverSocket.accept(); + final ProtocolHandler pHandler = + (ProtocolHandler) handler.clone(); + pHandler.setClientSocket(clientSocket); + Thread t = new Thread(pHandler); + synchronized (clients) { + clients.add(t); + } + t.start(); + } catch (final IOException e) { + //e.printStackTrace(); + } catch (NullPointerException nex) { + // serverSocket can be set to null before we could check + } + } + } finally { + quit(); + } + } + + /** + * Return number of clients ever created. + */ + public int clientCount() { + synchronized (clients) { + // isListening creates a client that we don't count + return clients.size() - 1; + } + } + + /** + * Wait for at least n clients to terminate. + */ + public void waitForClients(int n) { + if (n > clientCount()) + throw new RuntimeException("not that many clients"); + for (; ; ) { + int num = -1; // ignore isListening client + synchronized (clients) { + for (Thread t : clients) { + if (!t.isAlive()) { + if (++num >= n) + return; + } + } + } + try { + Thread.sleep(100); + } catch (InterruptedException ex) { + } + } + } + + private boolean isListening(int port) { + try { + Socket s = new Socket(); + s.connect(new InetSocketAddress("localhost", port), 100); + // it's listening! + s.close(); + return true; + } catch (Exception ex) { + //System.out.println(ex); + } + return false; + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/test/TestSocketFactory.java b/net-mail/src/test/java/org/xbib/net/mail/test/test/TestSocketFactory.java new file mode 100644 index 0000000..667a3e9 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/test/TestSocketFactory.java @@ -0,0 +1,123 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.test; + +import javax.net.SocketFactory; +import java.io.IOException; +import java.net.InetAddress; +import java.net.Socket; +import java.net.UnknownHostException; + +/** + * A socket factory for testing that tracks whether it's being used. + *

+ * + * An instance of this factory can be set as the value of the + * mail.<protocol>.socketFactory property. + * + * @since JavaMail 1.5.3 + * @author Stephan Sann + * @author Bill Shannon + */ +public class TestSocketFactory extends SocketFactory { + + /** + * Holds a SocketFactory to pass all API-method-calls to + */ + private SocketFactory defaultFactory = null; + + /** + * Was a socket created? + */ + private boolean socketCreated; + + /** + * Initializes a new TestSocketFactory. + */ + public TestSocketFactory() { + // Get the default SocketFactory to delegate all API-calls to. + defaultFactory = SocketFactory.getDefault(); + } + + /** + * Was a socket created using one of the createSocket methods? + */ + public boolean getSocketCreated() { + return socketCreated; + } + + + // SocketFactory methods + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket() + */ + @Override + public synchronized Socket createSocket() throws IOException { + Socket socket = defaultFactory.createSocket(); + socketCreated = true; + return socket; + } + + /* (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 { + Socket socket = + defaultFactory.createSocket(inetaddress, i, inetaddress1, j); + socketCreated = true; + return socket; + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.net.InetAddress, int) + */ + @Override + public synchronized Socket createSocket(InetAddress inetaddress, int i) + throws IOException { + Socket socket = defaultFactory.createSocket(inetaddress, i); + socketCreated = true; + return socket; + } + + /* (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 { + Socket socket = defaultFactory.createSocket(s, i, inetaddress, j); + socketCreated = true; + return socket; + } + + /* (non-Javadoc) + * @see javax.net.SocketFactory#createSocket(java.lang.String, int) + */ + @Override + public synchronized Socket createSocket(String s, int i) + throws IOException, UnknownHostException { + Socket socket = defaultFactory.createSocket(s, i); + socketCreated = true; + return socket; + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/AddAddressHeaderTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/AddAddressHeaderTest.java new file mode 100644 index 0000000..5592f43 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/AddAddressHeaderTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; + +import java.util.Properties; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test that "add" methods for address headers result in only a single + * address header, per RFC 2822. + */ +public class AddAddressHeaderTest { + + private static Session s = Session.getInstance(new Properties()); + private static InternetAddress[] setList = new InternetAddress[1]; + private static InternetAddress[] addList = new InternetAddress[1]; + + static { + try { + setList[0] = new InternetAddress("me@example.com"); + addList[0] = new InternetAddress("you@example.com"); + } catch (MessagingException ex) { + } + } + + @Test + public void testFrom() throws Exception { + MimeMessage m = new MimeMessage(s); + m.setFrom(setList[0]); + m.addFrom(addList); + m.saveChanges(); + String[] h = m.getHeader("From"); + assertEquals(1, h.length); + } + + @Test + public void testTo() throws Exception { + testRecipients(Message.RecipientType.TO); + } + + @Test + public void testCc() throws Exception { + testRecipients(Message.RecipientType.CC); + } + + @Test + public void testBcc() throws Exception { + testRecipients(Message.RecipientType.BCC); + } + + private void testRecipients(Message.RecipientType type) throws Exception { + MimeMessage m = new MimeMessage(s); + m.setRecipients(type, setList); + m.addRecipients(type, addList); + m.saveChanges(); + // XXX - depends on RecipientType.toString + String[] h = m.getHeader(type.toString()); + assertEquals(1, h.length); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/AddFromTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/AddFromTest.java new file mode 100644 index 0000000..44a1aa1 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/AddFromTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.Session; +import jakarta.mail.internet.AddressException; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; + +import java.util.Properties; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test the MimeMultipart.addFrom method, for bug 5057742. + */ +public class AddFromTest { + + private static final Session s = Session.getInstance(new Properties()); + private static final String ADDR = "a@example.com"; + private static final InternetAddress iaddr; + private static final InternetAddress[] addresses; + + static { + InternetAddress ia = null; + try { + ia = new InternetAddress(ADDR); + } catch (AddressException ex) { + // can't happen + } finally { + iaddr = ia; + } + addresses = new InternetAddress[]{iaddr}; + } + + @Test + public void testNoFrom() throws Exception { + MimeMessage m = new MimeMessage(s); + m.addFrom(addresses); + assertEquals(1, m.getHeader("From").length); + assertEquals( ADDR, m.getHeader("From", ",")); + } + + @Test + public void testOneFrom() throws Exception { + MimeMessage m = new MimeMessage(s); + m.setFrom(iaddr); + m.addFrom(addresses); + assertEquals(1, m.getHeader("From").length); + assertEquals("From header", ADDR + ", " + ADDR, + m.getHeader("From", ",")); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/AllowEncodedMessagesTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/AllowEncodedMessagesTest.java new file mode 100644 index 0000000..33c94ad --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/AllowEncodedMessagesTest.java @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.BodyPart; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.AsciiStringInputStream; + +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test "mail.mime.allowencodedmessages" System property. + */ +public class AllowEncodedMessagesTest { + + private static Session s = Session.getInstance(new Properties()); + + @BeforeAll + public static void before() { + System.out.println("AllowEncodedMessages"); + System.setProperty("mail.mime.allowencodedmessages", "true"); + } + + @Test + public void testEncodedMessages() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + BodyPart bp = mp.getBodyPart(0); + assertEquals("message/rfc822", bp.getContentType()); + + MimeMessage m2 = (MimeMessage) bp.getContent(); + assertEquals("text/plain", m2.getContentType()); + assertEquals("test message\r\n", m2.getContent()); + } + + @AfterAll + public static void after() { + // should be unnecessary + System.clearProperty("mail.mime.allowencodedmessages"); + } + + private static MimeMessage createMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"-\"\n" + + "\n" + + "---\n" + + "Content-Type: message/rfc822\n" + + "Content-Transfer-Encoding: base64\n" + + "\n" + + "TWltZS1WZXJzaW9uOiAxLjANClN1YmplY3Q6IH" + + "Rlc3QNCkNvbnRlbnQtVHlwZTogdGV4dC9wbGFp\n" + + "bg0KDQp0ZXN0IG1lc3NhZ2UNCg==\n" + + "\n" + + "-----\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/BASE64Test.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/BASE64Test.java new file mode 100644 index 0000000..3cd113f --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/BASE64Test.java @@ -0,0 +1,321 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.util; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.util.BASE64DecoderStream; +import org.xbib.net.mail.util.BASE64EncoderStream; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Base64; +import java.util.Random; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test base64 encoding/decoding. + * + * @author Bill Shannon + */ + +public class BASE64Test { + + @Test + public void test() throws IOException { + // test a range of buffer sizes + for (int bufsize = 1; bufsize < 100; bufsize++) { + //System.out.println("Buffer size: " + bufsize); + byte[] buf = new byte[bufsize]; + + // test a set of patterns + + // first, all zeroes + Arrays.fill(buf, (byte) 0); + test("Zeroes", buf); + + // now, all ones + Arrays.fill(buf, (byte) 0xff); + test("Ones", buf); + + // now, small integers + for (int i = 0; i < bufsize; i++) + buf[i] = (byte) i; + test("Ints", buf); + + // finally, random numbers + Random rnd = new Random(); + rnd.nextBytes(buf); + test("Random", buf); + } + } + + /** + * Encode and decode the buffer and check that we get back the + * same data. Encoding is done both with the static encode + * method and using the encoding stream. Likewise, decoding + * is done both with the static decode method and using the + * decoding stream. Check all combinations. + */ + private static void test(String name, byte[] buf) throws IOException { + // first encode and decode with method + byte[] encoded = Base64.getEncoder().encode(buf); + byte[] nbuf = Base64.getDecoder().decode(encoded); + compare(name, "method", buf, nbuf); + + // encode with stream, compare with method encoded version + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + BASE64EncoderStream os = + new BASE64EncoderStream(bos, Integer.MAX_VALUE); + os.write(buf); + os.flush(); + os.close(); + byte[] sbuf = bos.toByteArray(); + compare(name, "encoded", encoded, sbuf); + + // encode with stream, decode with method + nbuf = Base64.getDecoder().decode(sbuf); + compare(name, "stream->method", buf, nbuf); + + // encode with stream, decode with stream + ByteArrayInputStream bin = new ByteArrayInputStream(sbuf); + BASE64DecoderStream in = new BASE64DecoderStream(bin); + readAll(in, nbuf, nbuf.length); + compare(name, "stream", buf, nbuf); + + // encode with method, decode with stream + for (int i = 1; i <= nbuf.length; i++) { + bin = new ByteArrayInputStream(encoded); + in = new BASE64DecoderStream(bin); + readAll(in, nbuf, i); + compare(name, "method->stream " + i, buf, nbuf); + } + + // encode with stream, decode with stream, many buffers + + // first, fill the output with multiple buffers, up to the limit + int limit = 10000; // more than 8K + bos = new ByteArrayOutputStream(); + os = new BASE64EncoderStream(bos); + for (int size = 0, blen = buf.length; size < limit; size += blen) { + if (size + blen > limit) { + blen = limit - size; + // write out partial buffer, starting at non-zero offset + os.write(buf, buf.length - blen, blen); + } else + os.write(buf); + } + os.flush(); + os.close(); + + // read the encoded output and check the line length + String type = "big stream"; // for error messages below + sbuf = bos.toByteArray(); + bin = new ByteArrayInputStream(sbuf); + byte[] inbuf = new byte[78]; + for (int size = 0, blen = 76; size < limit; size += blen) { + if (size + blen > limit) { + blen = limit - size; + int n = bin.read(inbuf, 0, blen); + assertEquals(blen, n); + } else { + int n = bin.read(inbuf, 0, blen + 2); + assertEquals(blen + 2, n); + assertTrue(nbuf[blen] == (byte) '\r' && inbuf[blen + 1] == (byte) '\n'); + } + } + + // decode the output and check the data + bin = new ByteArrayInputStream(sbuf); + in = new BASE64DecoderStream(bin); + inbuf = new byte[buf.length]; + for (int size = 0, blen = buf.length; size < limit; size += blen) { + if (size + blen > limit) + blen = limit - size; + int n = in.read(nbuf, 0, blen); + assertEquals(blen, n); + if (blen != buf.length) { + // have to compare with end of original buffer + byte[] cbuf = new byte[blen]; + System.arraycopy(buf, buf.length - blen, cbuf, 0, blen); + // need a version of the read buffer that's the right size + byte[] cnbuf = new byte[blen]; + System.arraycopy(nbuf, 0, cnbuf, 0, blen); + compare(name, type, cbuf, cnbuf); + } else { + compare(name, type, buf, nbuf); + } + } + } + + private static byte[] origLine; + private static byte[] encodedLine; + + static { + origLine = + "000000000000000000000000000000000000000000000000000000000". + getBytes(StandardCharsets.US_ASCII); + encodedLine = + ("MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw" + + "MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAw" + "\r\n"). + getBytes(StandardCharsets.US_ASCII); + } + + /** + * Test that CRLF is inserted at the right place. + * Test combinations of array writes of different sizes + * and single byte writes. + */ + @Test + public void testLineLength() throws Exception { + for (int i = 0; i < origLine.length; i++) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + OutputStream os = new BASE64EncoderStream(bos); + os.write(origLine, 0, i); + os.write(origLine, i, origLine.length - i); + os.write((byte) '0'); + os.flush(); + os.close(); + + byte[] line = new byte[encodedLine.length]; + System.arraycopy(bos.toByteArray(), 0, line, 0, line.length); + assertArrayEquals(encodedLine, line); + } + + for (int i = 0; i < origLine.length; i++) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + OutputStream os = new BASE64EncoderStream(bos); + os.write(origLine, 0, i); + os.write(origLine, i, origLine.length - i); + os.write(origLine); + os.flush(); + os.close(); + + byte[] line = new byte[encodedLine.length]; + System.arraycopy(bos.toByteArray(), 0, line, 0, line.length); + assertArrayEquals(encodedLine, line); + } + + for (int i = 1; i < 5; i++) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + OutputStream os = new BASE64EncoderStream(bos); + for (int j = 0; j < i; j++) + os.write((byte) '0'); + os.write(origLine, i, origLine.length - i); + os.write((byte) '0'); + os.flush(); + os.close(); + + byte[] line = new byte[encodedLine.length]; + System.arraycopy(bos.toByteArray(), 0, line, 0, line.length); + assertArrayEquals(encodedLine, line); + } + for (int i = origLine.length - 5; i < origLine.length; i++) { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + + OutputStream os = new BASE64EncoderStream(bos); + os.write(origLine, 0, i); + for (int j = 0; j < origLine.length - i; j++) + os.write((byte) '0'); + os.write((byte) '0'); + os.flush(); + os.close(); + + byte[] line = new byte[encodedLine.length]; + System.arraycopy(bos.toByteArray(), 0, line, 0, line.length); + assertArrayEquals(encodedLine, line); + } + } + + @Test + public void testReadZeroBytes() throws Exception { + byte[] decoded = new byte[10000]; + for (int i = 0; i < 1000; i++) + decoded[i] = (byte) 'A'; + byte[] encoded = Base64.getEncoder().encode(decoded); + // Exceed InputStream.DEFAULT_BUFFER_SIZE + assertTrue(decoded.length > 8192); + BASE64DecoderStream sut = + new BASE64DecoderStream(new ByteArrayInputStream(encoded)); + // XXX - should test this using something equivalent to JDK 9's + // InputStream.readAllBytes, but for now... + int n = sut.read(decoded, 0, 0); + assertEquals(n, 0); + + // Exercise + //byte[] result = sut.readAllBytes(); + // Verify + //assertArrayEquals(decoded, result); + } + + /** + * Fill the buffer from the stream. + */ + private static void readAll(InputStream in, byte[] buf, int readsize) + throws IOException { + int need = buf.length; + int off = 0; + int got; + while (need > 0) { + got = in.read(buf, off, Math.min(need, readsize)); + if (got <= 0) + break; + off += got; + need -= got; + } + if (need != 0) + System.out.println("couldn't read all bytes"); + } + + /** + * Compare the two buffers. + */ + private static void compare(String name, String type, + byte[] buf, byte[] nbuf) { + /* + if (nbuf.length != buf.length) { + System.out.println(name + ": " + type + + " decoded array size wrong: " + + "got " + nbuf.length + ", expected " + buf.length); + dump(name + " buf", buf); + dump(name + " nbuf", nbuf); + } + */ + assertEquals(buf.length, nbuf.length); + for (int i = 0; i < buf.length; i++) { + assertEquals(buf[i], nbuf[i]); + } + } + + /** + * Dump the contents of the buffer. + */ + private static void dump(String name, byte[] buf) { + System.out.println(name); + for (int i = 0; i < buf.length; i++) + System.out.println(buf[i]); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/ContentTypeCleanerTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/ContentTypeCleanerTest.java new file mode 100644 index 0000000..9a14782 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/ContentTypeCleanerTest.java @@ -0,0 +1,106 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.BodyPart; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.internet.MimePart; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.AsciiStringInputStream; + +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test the "mail.mime.contenttypehandler" property. + */ +public class ContentTypeCleanerTest { + + private static Session s = Session.getInstance(new Properties()); + + @BeforeAll + public static void before() { + System.out.println("ContentTypeCleaner"); + System.setProperty("mail.mime.contenttypehandler", + ContentTypeCleanerTest.class.getName()); + } + + @Test + public void testGarbage() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + BodyPart bp = mp.getBodyPart(0); + assertEquals("text/plain", bp.getContentType()); + assertEquals("first part\n", bp.getContent()); + } + + @Test + public void testValid() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + BodyPart bp = mp.getBodyPart(1); + assertEquals("text/plain; charset=iso-8859-1", bp.getContentType()); + assertEquals("second part\n", bp.getContent()); + } + + @Test + public void testEmpty() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + BodyPart bp = mp.getBodyPart(2); + assertEquals("text/plain", bp.getContentType()); + assertEquals("third part\n", bp.getContent()); + } + + public static String cleanContentType(MimePart mp, String contentType) { + if (contentType == null) + return null; + if (contentType.equals("complete garbage")) + return "text/plain"; + return contentType; + } + + private static MimeMessage createMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"-\"\n" + + "\n" + + "preamble\n" + + "---\n" + + "Content-Type: complete garbage\n" + + "\n" + + "first part\n" + + "\n" + + "---\n" + + "Content-Type: text/plain; charset=iso-8859-1\n" + + "\n" + + "second part\n" + + "\n" + + "---\n" + + "\n" + + "third part\n" + + "\n" + + "-----\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/DecodeParametersTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/DecodeParametersTest.java new file mode 100644 index 0000000..a42dc42 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/DecodeParametersTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Test that the "mail.mime.decodeparameters" System property + * causes the parameters to be properly decoded. + */ +public class DecodeParametersTest extends ParameterListDecode { + + @BeforeAll + public static void before() { + System.setProperty("mail.mime.decodeparameters", "true"); + } + + @Test + public void testDecode() throws Exception { + testDecode("paramdata"); + } + + @AfterAll + public static void after() { + // should be unnecessary + System.clearProperty("mail.mime.decodeparameters"); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/EncodeFileNameNoEncodeParametersTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/EncodeFileNameNoEncodeParametersTest.java new file mode 100644 index 0000000..3d2f13c --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/EncodeFileNameNoEncodeParametersTest.java @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2015, 2023 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 org.xbib.net.mail.test.util; + +import org.junit.jupiter.api.BeforeAll; + +/** + * Test "mail.mime.encodefilename" System property set to "true" + * and "mail.mime.encodeparameters" set to "false". + */ +public class EncodeFileNameNoEncodeParametersTest extends EncodeFileNameTest { + + @BeforeAll + public static void before() { + System.out.println("EncodeFileNameNoEncodeParameters"); + System.setProperty("mail.mime.charset", "utf-8"); + System.setProperty("mail.mime.encodefilename", "true"); + System.setProperty("mail.mime.encodeparameters", "false"); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/EncodeFileNameTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/EncodeFileNameTest.java new file mode 100644 index 0000000..e50bbe5 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/EncodeFileNameTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2015, 2023 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 org.xbib.net.mail.test.util; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test "mail.mime.encodefilename" System property set. + */ +public class EncodeFileNameTest extends NoEncodeFileNameTest { + + // depends on exactly how MimeUtility.encodeText splits long words + private static String expected1 = + "=?utf-8?B?w4DDgcOFw4bDgMOBw4XDhsOHw4jDicOKw4vDjMONw47Dj8OQw4DDgcOF?="; + private static String expected2 = + "=?utf-8?B?w4bDh8OIw4nDisOLw4zDjcOOw4/DkMORw5LDk8OUw5XDlsOYw5nDmsObw5w=?="; + private static String expected3 = + "=?utf-8?B?w53DnsOfw6DDocOiw6PDpMOlw6bDp8Oow6nDqsOrw6zDrcOuw6/DsMOx?="; + private static String expected4 = + "=?utf-8?B?w7LDs8O0w7XDtsO4w7nDusO7w7zDvcO+w7/DgMOBw4XDhsOHLmRvYw==?="; + + @BeforeAll + public static void before() { + System.out.println("EncodeFileName"); + System.setProperty("mail.mime.charset", "utf-8"); + System.setProperty("mail.mime.encodefilename", "true"); + // assume mail.mime.encodeparameters defaults to true + System.clearProperty("mail.mime.encodeparameters"); + } + + @Test + @Override + public void test() throws Exception { + MimeBodyPartPublicUpdateHeaders mbp = new MimeBodyPartPublicUpdateHeaders(); + mbp.setText("test"); + mbp.setFileName(fileName); + mbp.updateHeaders(); + String h = mbp.getHeader("Content-Type", ""); + assertTrue(h.contains("name=")); + assertTrue(h.contains(expected1)); + assertTrue(h.contains(expected2)); + assertTrue(h.contains(expected3)); + assertTrue(h.contains(expected4)); + h = mbp.getHeader("Content-Disposition", ""); + assertTrue(h.contains("filename=")); + assertTrue(h.contains(expected1)); + assertTrue(h.contains(expected2)); + assertTrue(h.contains(expected3)); + assertTrue(h.contains(expected4)); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/GetLocalAddressTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/GetLocalAddressTest.java new file mode 100644 index 0000000..fc4379a --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/GetLocalAddressTest.java @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Properties; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test the InternetAddress.getLocalAddress() method. + */ +public class GetLocalAddressTest { + + private static String localhost; + + static { + try { + localhost = InetAddress.getLocalHost().getCanonicalHostName(); + // if the host name and host address are the same, the name + // is really an address and we need to convert it to an + // internet address literal to use it in an email address + if (localhost.equals(InetAddress.getLocalHost().getHostAddress())) + localhost = "[" + localhost + "]"; + } catch (UnknownHostException ex) { + localhost = "localhost"; + } + } + + @Test + public void testUserName() throws Exception { + System.setProperty("user.name", "Joe"); + InternetAddress ia = InternetAddress.getLocalAddress(null); + assertEquals("Joe@" + localhost, ia.getAddress()); + } + + @Test + public void testUserNameSession() throws Exception { + System.setProperty("user.name", "Joe"); + Session s = Session.getInstance(new Properties()); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("Joe@" + localhost, ia.getAddress()); + } + + @Test + public void testMailFrom() throws Exception { + System.setProperty("user.name", "Joe"); + Properties p = new Properties(); + p.setProperty("mail.from", "Bob@home"); + Session s = Session.getInstance(p); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("Bob@home", ia.getAddress()); + } + + @Test + public void testMailFromAddress() throws Exception { + System.setProperty("user.name", "Joe"); + Properties p = new Properties(); + p.setProperty("mail.from", "Bob "); + Session s = Session.getInstance(p); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("Bob@home", ia.getAddress()); + } + + @Test + public void testMailUser() throws Exception { + System.setProperty("user.name", "Joe"); + Properties p = new Properties(); + p.setProperty("mail.user", "Bob"); + Session s = Session.getInstance(p); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("Bob@" + localhost, ia.getAddress()); + } + + @Test + public void testMailUserSpace() throws Exception { + System.setProperty("user.name", "Joe"); + Properties p = new Properties(); + p.setProperty("mail.user", "NETWORK SERVICE"); + Session s = Session.getInstance(p); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("\"NETWORK SERVICE\"@" + localhost, ia.getAddress()); + } + + @Test + public void testMailHost() throws Exception { + System.setProperty("user.name", "Joe"); + Properties p = new Properties(); + p.setProperty("mail.user", "Bob"); + p.setProperty("mail.host", "home"); + Session s = Session.getInstance(p); + InternetAddress ia = InternetAddress.getLocalAddress(s); + assertEquals("Bob@home", ia.getAddress()); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/InternetHeadersTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/InternetHeadersTest.java new file mode 100644 index 0000000..c66fd0f --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/InternetHeadersTest.java @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2011, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.Header; +import jakarta.mail.internet.InternetHeaders; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.AsciiStringInputStream; + +import java.util.Enumeration; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test the InternetHeaders class. + */ +public class InternetHeadersTest { + + private static final String initialWhitespaceHeader = + " \r\nSubject: test\r\n\r\n"; + private static final String initialContinuationHeader = + " Subject: test\r\n\r\n"; + + /** + * Test that a continuation line is handled properly. + */ + @Test + public void testContinuationLine() throws Exception { + String header = "Subject: a\r\n b\r\n\r\n"; + InternetHeaders ih = new InternetHeaders( + new AsciiStringInputStream(header)); + assertEquals(1, ih.getHeader("Subject").length); + assertEquals("a\r\n b", ih.getHeader("Subject")[0]); + } + + /** + * Test that a whitespace line at the beginning is ignored. + */ + @Test + public void testInitialWhitespaceLineConstructor() throws Exception { + InternetHeaders ih = new InternetHeaders( + new AsciiStringInputStream(initialWhitespaceHeader)); + testInitialWhitespaceLine(ih); + } + + /** + * Test that a whitespace line at the beginning is ignored. + */ + @Test + public void testInitialWhitespaceLineLoad() throws Exception { + InternetHeaders ih = new InternetHeaders(); + ih.load(new AsciiStringInputStream(initialWhitespaceHeader)); + testInitialWhitespaceLine(ih); + } + + private void testInitialWhitespaceLine(InternetHeaders ih) + throws Exception { + assertEquals(1, ih.getHeader("Subject").length); + assertEquals("test", ih.getHeader("Subject")[0]); + Enumeration

e = ih.getAllHeaders(); + while (e.hasMoreElements()) { + Header h = e.nextElement(); + assertEquals("Subject", h.getName()); + assertEquals("test", h.getValue()); + } + } + + /** + * Test that a continuation line at the beginning is handled. + */ + @Test + public void testInitialContinuationLineConstructor() throws Exception { + InternetHeaders ih = new InternetHeaders( + new AsciiStringInputStream(initialContinuationHeader)); + testInitialContinuationLine(ih); + } + + /** + * Test that a continuation line at the beginning is handled. + */ + @Test + public void testInitialContinuationLineLoad() throws Exception { + InternetHeaders ih = new InternetHeaders(); + ih.load(new AsciiStringInputStream(initialContinuationHeader)); + testInitialContinuationLine(ih); + } + + private void testInitialContinuationLine(InternetHeaders ih) + throws Exception { + assertEquals(1, ih.getHeader("Subject").length); + assertEquals("test", ih.getHeader("Subject")[0]); + Enumeration
e = ih.getAllHeaders(); + while (e.hasMoreElements()) { + Header h = e.nextElement(); + assertEquals("Subject", h.getName()); + assertEquals("test", h.getValue()); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeBodyPartTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeBodyPartTest.java new file mode 100644 index 0000000..0214539 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeBodyPartTest.java @@ -0,0 +1,210 @@ +/* + * Copyright (c) 2011, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.activation.DataHandler; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.StreamProvider.EncoderTypes; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.AsciiStringInputStream; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test the MimeBodyPart class. + */ +public class MimeBodyPartTest { + + private static String[] languages = new String[]{ + "language1", "language2", "language3", "language4", "language5", + "language6", "language7", "language8", "language9", "language10", + "language11", "language12", "language13", "language14", "language15" + }; + + /** + * Test that the Content-Language header is properly folded + * if there are a lot of languages. + */ + @Test + public void testContentLanguageFold() throws Exception { + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setContentLanguage(languages); + String header = mbp.getHeader("Content-Language", ","); + assertTrue(header.indexOf("\r\n") > 0); + + String[] langs = mbp.getContentLanguage(); + assertArrayEquals(languages, langs); + } + + /** + * Test that copying a DataHandler from one message to another + * has the desired effect. + */ + @Test + public void testCopyDataHandler() throws Exception { + Session s = Session.getInstance(new Properties()); + // create a message and extract the DataHandler for a part + MimeMessage orig = createMessage(s); + MimeMultipart omp = (MimeMultipart) orig.getContent(); + MimeBodyPart obp = (MimeBodyPart) omp.getBodyPart(0); + DataHandler dh = obp.getDataHandler(); + // create a new message and use the DataHandler + MimeMessage msg = new MimeMessage(s); + MimeMultipart mp = new MimeMultipart(); + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setDataHandler(dh); + mp.addBodyPart(mbp); + msg.setContent(mp); + // depend on copy constructor streaming the data + msg = new MimeMessage(msg); + mp = (MimeMultipart) msg.getContent(); + mbp = (MimeBodyPart) mp.getBodyPart(0); + assertEquals("text/x-test", mbp.getContentType()); + assertEquals(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder(), mbp.getEncoding()); + assertEquals("test part", getString(mbp.getInputStream())); + } + + /** + * Test that copying a DataHandler from one message to another + * by setting the "dh" field in a subclass has the desired effect. + */ + @Test + public void testSetDataHandler() throws Exception { + Session s = Session.getInstance(new Properties()); + // create a message and extract the DataHandler for a part + MimeMessage orig = createMessage(s); + MimeMultipart omp = (MimeMultipart) orig.getContent(); + MimeBodyPart obp = (MimeBodyPart) omp.getBodyPart(0); + final DataHandler odh = obp.getDataHandler(); + // create a new message and use the DataHandler + MimeMessage msg = new MimeMessage(s); + MimeMultipart mp = new MimeMultipart(); + MimeBodyPart mbp = new MimeBodyPart() { + { + dh = odh; + } + }; + mp.addBodyPart(mbp); + msg.setContent(mp); + // depend on copy constructor streaming the data + msg = new MimeMessage(msg); + mp = (MimeMultipart) msg.getContent(); + mbp = (MimeBodyPart) mp.getBodyPart(0); + assertEquals("text/x-test", mbp.getContentType()); + assertEquals(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder(), mbp.getEncoding()); + assertEquals("test part", getString(mbp.getInputStream())); + } + + /** + * Test that a MimeBodyPart created from a stream with unencoded data + * will have the data be encoded when the data is copied to another + * MimeBodyPart by copying the DataHandler. + */ + @Test + public void testEncodingCopiedDataHandler() throws Exception { + String part = + "Content-Type: application/x-test\n" + + "\n" + + "\u0001\u0002\u0003" + + "\n"; + MimeBodyPart mbp = new MimeBodyPart(new AsciiStringInputStream(part)); + MimeBodyPart mbp2 = new MimeBodyPart() { + @Override + public void setDataHandler(DataHandler dh) + throws MessagingException { + super.setDataHandler(dh); + updateHeaders(); + } + }; + mbp2.setDataHandler(mbp.getDataHandler()); + assertEquals(EncoderTypes.BASE_64.getEncoder(), mbp2.getEncoding()); + // ensure the data is correct by reading the first byte + InputStream in = mbp2.getInputStream(); + assertEquals(1, in.read()); + in.close(); + } + + /** + * Test that isMimeType does something reasonable even if the + * Content-Type header can't be parsed because of a bad parameter. + */ + @Test + public void testIsMimeTypeBadParameter() throws Exception { + String part = + "Content-Type: application/x-test; type=a/b\n" + + "\n" + + "\n"; + MimeBodyPart mbp = new MimeBodyPart(new AsciiStringInputStream(part)); + assertTrue(mbp.isMimeType("application/x-test")); + assertTrue(mbp.isMimeType("application/*")); + assertFalse(mbp.isMimeType("application/test")); + } + + /** + * Test that a Content-Transfer-Encoding header with no value doesn't + * cause an IOException. + */ + @Test + public void testEmptyContentTransferEncoding() throws Exception { + String part = + "Content-Type: text/plain; charset=\"us-ascii\"\n" + + "Content-Transfer-Encoding: \n" + + "\n" + + "test" + + "\n"; + MimeBodyPart mbp = new MimeBodyPart(new AsciiStringInputStream(part)); + assertEquals("empty C-T-E value", null, mbp.getEncoding()); + assertEquals("test\n", mbp.getContent()); + } + + + private static MimeMessage createMessage(Session s) + throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"-\"\n" + + "\n" + + "preamble\n" + + "---\n" + + "Content-Type: text/x-test\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "\n" + + "test part\n" + + "\n" + + "-----\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } + + private static String getString(InputStream is) throws IOException { + BufferedReader r = new BufferedReader(new InputStreamReader(is)); + return r.readLine(); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMessageTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMessageTest.java new file mode 100644 index 0000000..5297d7d --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMessageTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.activation.DataHandler; +import jakarta.mail.Address; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.NewsAddress; +import jakarta.mail.util.StreamProvider.EncoderTypes; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.AsciiStringInputStream; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.util.Enumeration; +import java.util.Properties; + +import static jakarta.mail.Message.RecipientType.TO; +import static jakarta.mail.internet.MimeMessage.RecipientType.NEWSGROUPS; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test MimeMessage methods. + * + * XXX - just a beginning... + * + * @author Bill Shannon + */ +public class MimeMessageTest { + + private static final Session s = Session.getInstance(new Properties()); + + /** + * Test that setRecipients with a null string address removes the header. + * (Bug 7021190) + */ + @Test + public void testSetRecipientsStringNull() throws Exception { + String addr = "joe@example.com"; + MimeMessage m = new MimeMessage(s); + m.setRecipients(TO, addr); + assertEquals(addr, m.getRecipients(TO)[0].toString()); + m.setRecipients(TO, (String) null); + assertArrayEquals(null, m.getRecipients(TO)); + } + + /** + * Test that setRecipient with a null string address removes the header. + * (Bug 7536) + */ + @Test + public void testSetRecipientStringNull() throws Exception { + String addr = "joe@example.com"; + MimeMessage m = new MimeMessage(s); + m.setRecipient(TO, new InternetAddress(addr)); + assertEquals("To: is set", addr, m.getRecipients(TO)[0].toString()); + m.setRecipient(TO, (Address) null); + assertArrayEquals(null, m.getRecipients(TO)); + } + + /** + * Test that setFrom with a null address removes the header. + * (Bug E 456) + */ + @Test + public void testSetFromStringNull() throws Exception { + String addr = "joe@example.com"; + MimeMessage m = new MimeMessage(s); + m.setFrom(new InternetAddress(addr)); + assertEquals("From: is set", addr, m.getFrom()[0].toString()); + m.setFrom((Address) null); + assertArrayEquals(null, m.getFrom()); + } + + /** + * Test that setSender with a null address removes the header. + * (Bug E 456) + */ + @Test + public void testSetSenderStringNull() throws Exception { + String addr = "joe@example.com"; + MimeMessage m = new MimeMessage(s); + m.setSender(new InternetAddress(addr)); + assertEquals("Sender: is set", addr, m.getSender().toString()); + m.setSender((Address) null); + assertNull(m.getSender()); + } + + /** + * Test that setFrom with an address containing a newline is folded + * properly. + * (Bug 7529) + */ + @Test + public void testSetFromFold() throws Exception { + InternetAddress addr = new InternetAddress("joe@bad.com", "Joe\r\nBad"); + MimeMessage m = new MimeMessage(s); + m.setFrom(addr); + assertEquals("Joe\r\n Bad ", m.getHeader("From", null)); + } + + /** + * Test that setSender with an address containing a newline is folded + * properly. + * (Bug 7529) + */ + @Test + public void testSetSenderFold() throws Exception { + InternetAddress addr = new InternetAddress("joe@bad.com", "Joe\r\nBad"); + MimeMessage m = new MimeMessage(s); + m.setSender(addr); + assertEquals("Joe\r\n Bad ", m.getHeader("Sender", null)); + } + + /** + * Test that setRecipient with a newsgroup address containing a newline is + * handled properly. + * (Bug 7529) + */ + @Test + public void testSetNewsgroupWhitespace() throws Exception { + NewsAddress addr = new NewsAddress("alt.\r\nbad"); + MimeMessage m = new MimeMessage(s); + m.setRecipient(NEWSGROUPS, addr); + assertEquals("alt.bad", m.getHeader("Newsgroups", null)); + } + + /** + * Test that setRecipients with many newsgroup addresses is folded properly. + * (Bug 7529) + */ + @Test + public void testSetNewsgroupFold() throws Exception { + NewsAddress[] longng = NewsAddress.parse( + "alt.loooooooooooooooooooooooooooooooooooooooooooooooooong," + + "alt.verylongggggggggggggggggggggggggggggggggggggggggggggg"); + MimeMessage m = new MimeMessage(s); + m.setRecipients(NEWSGROUPS, longng); + assertTrue(m.getHeader("Newsgroups", null).indexOf("\r\n\t") > 0); + } + + /** + * Test that newsgroups can be set and read back (even if folded). + */ + @Test + public void testSetGetNewsgroups() throws Exception { + NewsAddress[] longng = NewsAddress.parse( + "alt.loooooooooooooooooooooooooooooooooooooooooooooooooong," + + "alt.verylongggggggggggggggggggggggggggggggggggggggggggggg"); + MimeMessage m = new MimeMessage(s); + m.setRecipients(NEWSGROUPS, longng); + assertArrayEquals(longng, m.getRecipients(NEWSGROUPS)); + } + + /** + * Test that copying a DataHandler from one message to another + * has the desired effect. + */ + @Test + public void testCopyDataHandler() throws Exception { + Session s = Session.getInstance(new Properties()); + // create a message and extract the DataHandler + MimeMessage orig = createMessage(s); + DataHandler dh = orig.getDataHandler(); + // create a new message and use the DataHandler + MimeMessage msg = new MimeMessage(s); + msg.setDataHandler(dh); + // depend on copy constructor streaming the data + msg = new MimeMessage(msg); + assertEquals("text/x-test", msg.getContentType()); + assertEquals(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder(), msg.getEncoding()); + assertEquals("test message", getString(msg.getInputStream())); + } + + /** + * Test that copying a DataHandler from one message to another + * by setting the "dh" field in a subclass has the desired effect. + */ + @Test + public void testSetDataHandler() throws Exception { + Session s = Session.getInstance(new Properties()); + // create a message and extract the DataHandler for a part + MimeMessage orig = createMessage(s); + final DataHandler odh = orig.getDataHandler(); + // create a new message and use the DataHandler + MimeMessage msg = new MimeMessage(s) { + { + dh = odh; + } + }; + // depend on copy constructor streaming the data + msg = new MimeMessage(msg); + assertEquals("text/x-test", msg.getContentType()); + assertEquals(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder(), msg.getEncoding()); + assertEquals("test message", getString(msg.getInputStream())); + } + + /** + * Test that address headers account for the header length when folding. + */ + @Test + public void testAddressHeaderFolding() throws Exception { + Session s = Session.getInstance(new Properties()); + MimeMessage msg = new MimeMessage(s); + InternetAddress[] addrs = InternetAddress.parse( + "long-address1@example.com, long-address2@example.com, joe@foobar.com"); + msg.setReplyTo(addrs); // use Reply-To because it's a long header name + Enumeration e + = msg.getMatchingHeaderLines(new String[]{"Reply-To"}); + String line = e.nextElement(); + int npos = line.indexOf("\r"); + // was the line folded where we expected? + assertTrue(npos > 9 && npos <= 77 && npos < line.length()); + } + + private static MimeMessage createMessage(Session s) + throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: text/x-test\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "\n" + + "test message\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } + + private static String getString(InputStream is) throws IOException { + BufferedReader r = new BufferedReader(new InputStreamReader(is)); + return r.readLine(); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartBCSIndexTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartBCSIndexTest.java new file mode 100644 index 0000000..b900216 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartBCSIndexTest.java @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; + +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * the Bad Character Shift table index use inconsistency between + * the 670th line and 823th line which leads to some problem in + * non-ascii situation, give a test here + * + * @author dslztx + */ +public class MimeMultipartBCSIndexTest { + + private String EMLContent = "From: dslztx@gmail.com \n" + + "To: dslztx \n" + + "Subject: bcs index test \n" + + "Date: Sat, 25 Aug 2018 08:35:14 +0800\n" + + "Content-Type: multipart/alternative;\n" + + "\tboundary=\"----=_000_6675�������=----\"\n" + + "\n" + + "This is a multi-part message in MIME format.\n" + + "\n" + + "------=_000_6675�������=----\n" + + "Content-Type: text/plain;\n" + + "\tcharset=\"utf-8\"\n" + + "Content-Transfer-Encoding: base64\n" + + "\n" + + "aGVsbG8gd29ybGQ=\n" + + "\n" + + "------=_000_6675�������=----\n" + + "Content-Type: text/html;\n" + + "\tcharset=\"utf-8\"\n" + + "Content-Transfer-Encoding: base64\n" + + "\n" + + "PGh0bWw+CjxoZWFkZXI+PHRpdGxlPlRoaXMgaXMgdGl0bGU8L3RpdGxlPjwvaGVhZGVyPgo8Ym9\n" + + "keT4KSGVsbG8gd29ybGQKPC9ib2R5Pgo8L2h0bWw+\n" + + "\n" + + "------=_000_6675�������=------"; + + @Test + public void testBCSTableIndexInconsistency() { + + try { + InputStream in = new ByteArrayInputStream(EMLContent.getBytes(StandardCharsets.ISO_8859_1)); + + Session session = Session.getDefaultInstance(new Properties()); + + MimeMessage mimeMessage = new MimeMessage(session, + in); + + MimeMultipart topMultipart = (MimeMultipart) mimeMessage.getContent(); + + assertTrue(topMultipart.getCount() == 2); + assertTrue(topMultipart.getBodyPart(0).getContent().equals("hello world")); + assertTrue(topMultipart.getBodyPart(1).getContent().equals("\n" + + "
This is title
\n" + + "\n" + + "Hello world\n" + + "\n" + + "")); + } catch (Exception e) { + e.printStackTrace(); + fail(); + } + } + +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartParseTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartParseTest.java new file mode 100644 index 0000000..70baa5f --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartParseTest.java @@ -0,0 +1,150 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.activation.DataHandler; +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.ByteArrayDataSource; +import jakarta.mail.util.SharedByteArrayInputStream; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.util.Date; +import java.util.Properties; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/* + * Test multipart parsing. + * + * @author Bill Shannon + */ + +public class MimeMultipartParseTest { + private static Session session = + Session.getInstance(new Properties(), null); + + private static final int maxsize = 10000; + private static final String data = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + @Test + public void testParse() throws Exception { + test(false); + } + + @Test + public void testParseShared() throws Exception { + test(true); + } + + /* + * Test a few potential boundary cases, then test a range. + * This is a compromise to make the run time of this test reasonable, + * although it still takes about 30 seconds, which is on the long side + * for a unit test. + */ + public void test(boolean shared) throws Exception { + testMessage(1, shared); + testMessage(2, shared); + testMessage(62, shared); + testMessage(63, shared); + testMessage(64, shared); + testMessage(65, shared); + testMessage(1023, shared); + testMessage(1024, shared); + testMessage(1025, shared); + for (int size = 8100; size <= maxsize; size++) + testMessage(size, shared); + } + + public void testMessage(int size, boolean shared) throws Exception { + //System.out.println("SIZE: " + size); + /* + * Construct a multipart message with a part of the + * given size. + */ + MimeMessage msg = new MimeMessage(session); + msg.setFrom(new InternetAddress("me@example.com")); + msg.setSubject("test multipart parsing"); + msg.setSentDate(new Date(0)); + MimeBodyPart mbp1 = new MimeBodyPart(); + mbp1.setText("main text\n"); + MimeBodyPart mbp3 = new MimeBodyPart(); + mbp3.setText("end text\n"); + MimeBodyPart mbp2 = new MimeBodyPart(); + byte[] part = new byte[size]; + for (int i = 0; i < size; i++) { + int j = i % 64; + if (j == 62) + part[i] = (byte) '\r'; + else if (j == 63) + part[i] = (byte) '\n'; + else + part[i] = (byte) data.charAt((j + i / 64) % 62); + } + mbp2.setDataHandler(new DataHandler( + new ByteArrayDataSource(part, "text/plain"))); + + MimeMultipart mp = new MimeMultipart(); + mp.addBodyPart(mbp1); + mp.addBodyPart(mbp2); + mp.addBodyPart(mbp3); + msg.setContent(mp); + msg.saveChanges(); + + /* + * Write the message out to a byte array. + */ + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + msg.writeTo(bos); + bos.close(); + byte[] buf = bos.toByteArray(); + + /* + * Construct a new message to parse the bytes. + */ + msg = new MimeMessage(session, shared ? + new SharedByteArrayInputStream(buf) : + new ByteArrayInputStream(buf)); + + // verify that the part content is correct + mp = (MimeMultipart) msg.getContent(); + mbp2 = (MimeBodyPart) mp.getBodyPart(1); + InputStream is = mbp2.getInputStream(); + int k = 0; + int c; + while ((c = is.read()) >= 0) { + int j = k % 64; + byte e; + if (j == 62) + e = (byte) '\r'; + else if (j == 63) + e = (byte) '\n'; + else + e = (byte) data.charAt((j + k / 64) % 62); + assertEquals(e, c); + k++; + } + assertEquals(size, k); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartPreambleTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartPreambleTest.java new file mode 100644 index 0000000..c946c7c --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartPreambleTest.java @@ -0,0 +1,118 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.BodyPart; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.Properties; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test the MIME multipart messages are parsed correctly and the + * correct preamble is returned no matter what line terminators + * are used. This test found some bugs in the way LineInputStream + * handled different line terminators. + * + * @author trejkaz@kenai.com + */ +public class MimeMultipartPreambleTest { + @SuppressWarnings({"SingleCharacterStringConcatenation"}) + private String THREE_PART_MAIL = + "From: user1@example.com\n" + + "To: user2@example.com\n" + + "Subject: Receipts\n" + + "Date: Wed, 14 Jul 2010 19:25:30 +1000\n" + + "MIME-Version: 1.0\n" + + "Content-Type: multipart/mixed;boundary=\"----=_NextPart_000_001C_01CB238A.52E35400\"\n" + + "\n" + + "This is a multi-part message in MIME format.\n" + + "\n" + + "------=_NextPart_000_001C_01CB238A.52E35400\n" + + "Content-Type: text/plain;charset=\"us-ascii\"\n" + + "Content-Transfer-Encoding: 7bit\n" + + "\n" + + "Hi.\n" + + "\n" + + "\n" + + "------=_NextPart_000_001C_01CB238A.52E35400\n" + + "Content-Type: application/pdf;name=\"Receipt 1.pdf\"\n" + + "Content-Transfer-Encoding: base64\n" + + "Content-Disposition: attachment;filename=\"Receipt 1.pdf\"\n" + + "\n" + + "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0ZURlY29k\n" + + "\n" + + "------=_NextPart_000_001C_01CB238A.52E35400\n" + + "Content-Type: application/pdf;name=\"Receipt 2.pdf\"\n" + + "Content-Transfer-Encoding: base64\n" + + "Content-Disposition: attachment;filename=\"Receipt 2.pdf\"\n" + + "\n" + + "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFIvRmlsdGVyIC9GbGF0ZURlY29k\n" + + "\n" + + "------=_NextPart_000_001C_01CB238A.52E35400--\n" + + "\n" + + "\n"; + + @Test + public void testUnixLines() throws Exception { + doThreePartMailTest(THREE_PART_MAIL); + } + + @Test + public void testWindowsLines() throws Exception { + doThreePartMailTest(THREE_PART_MAIL.replace("\n", "\r\n")); + } + + @Test + public void testMacLines() throws Exception { + doThreePartMailTest(THREE_PART_MAIL.replace('\n', '\r')); + } + + /** + * Performs a check that the multipart of the email was handled correctly. + * + * @param text the email text. + * @throws Exception if an error occurs. + */ + private void doThreePartMailTest(String text) throws Exception { + Session session = Session.getDefaultInstance(new Properties()); + MimeMessage mimeMessage = new MimeMessage(session, + new ByteArrayInputStream(text.getBytes(StandardCharsets.US_ASCII))); + + MimeMultipart topMultipart = (MimeMultipart) mimeMessage.getContent(); + assertEquals("This is a multi-part message in MIME format.", + topMultipart.getPreamble().trim()); + assertEquals(3, topMultipart.getCount()); + + BodyPart part1 = topMultipart.getBodyPart(0); + assertEquals("Wrong content type for part 1", + "text/plain;charset=\"us-ascii\"", part1.getContentType()); + + BodyPart part2 = topMultipart.getBodyPart(1); + assertEquals("Wrong content type for part 2", + "application/pdf;name=\"Receipt 1.pdf\"", part2.getContentType()); + + BodyPart part3 = topMultipart.getBodyPart(2); + assertEquals("Wrong content type for part 3", + "application/pdf;name=\"Receipt 2.pdf\"", part3.getContentType()); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartPropertyTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartPropertyTest.java new file mode 100644 index 0000000..1968b60 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeMultipartPropertyTest.java @@ -0,0 +1,194 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.AsciiStringInputStream; +import org.xbib.net.mail.test.test.NullOutputStream; + +import java.io.IOException; +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test the properties that control the MimeMultipart class. + * Since the properties are now read in the parse method, all + * these tests can be run in the same JVM. + */ +public class MimeMultipartPropertyTest { + + private static Session s = Session.getInstance(new Properties()); + + /** + * Clear all properties before each test. + */ + @BeforeEach + public void beforeTest() { + clearAll(); + } + + @Test + public void testBoundary() throws Exception { + MimeMessage m = createMessage("x", "x", true); + MimeMultipart mp = (MimeMultipart) m.getContent(); + assertEquals(mp.getCount(), 2); + } + + @Test + public void testBoundaryIgnore() throws Exception { + System.setProperty( + "mail.mime.multipart.ignoreexistingboundaryparameter", "true"); + MimeMessage m = createMessage("x", "-", true); + MimeMultipart mp = (MimeMultipart) m.getContent(); + assertEquals(mp.getCount(), 2); + } + + @Test + public void testBoundaryMissing() throws Exception { + MimeMessage m = createMessage(null, "x", true); + MimeMultipart mp = (MimeMultipart) m.getContent(); + assertEquals(mp.getCount(), 2); + } + + @Test // (expected = MessagingException.class) + public void testBoundaryMissingEx() throws Exception { + System.setProperty( + "mail.mime.multipart.ignoremissingboundaryparameter", "false"); + MimeMessage m = createMessage(null, "x", true); + MimeMultipart mp = (MimeMultipart) m.getContent(); + mp.getCount(); // throw exception + assertTrue(false); // never get here + } + + @Test + public void testEndBoundaryMissing() throws Exception { + MimeMessage m = createMessage("x", "x", false); + MimeMultipart mp = (MimeMultipart) m.getContent(); + assertEquals(mp.getCount(), 2); + } + + @Test // (expected = MessagingException.class) + public void testEndBoundaryMissingEx() throws Exception { + System.setProperty( + "mail.mime.multipart.ignoremissingendboundary", "false"); + MimeMessage m = createMessage("x", "x", false); + MimeMultipart mp = (MimeMultipart) m.getContent(); + mp.getCount(); // throw exception + assertTrue(false); // never get here + } + + @Test + public void testAllowEmpty() throws Exception { + System.setProperty("mail.mime.multipart.allowempty", "true"); + MimeMessage m = createEmptyMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + assertEquals(mp.getCount(), 0); + } + + @Test //(expected = MessagingException.class) + public void testAllowEmptyEx() throws Exception { + MimeMessage m = createEmptyMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + mp.getCount(); // throw exception + assertTrue(false); // never get here + } + + @Test + public void testAllowEmptyOutput() throws Exception { + System.setProperty("mail.mime.multipart.allowempty", "true"); + MimeMessage m = new MimeMessage(s); + MimeMultipart mp = new MimeMultipart(); + m.setContent(mp); + m.writeTo(new NullOutputStream()); + assertEquals(mp.getCount(), 0); + } + + @Test //(expected = IOException.class) + public void testAllowEmptyOutputEx() throws Exception { + MimeMessage m = new MimeMessage(s); + MimeMultipart mp = new MimeMultipart(); + m.setContent(mp); + m.writeTo(new NullOutputStream()); // throw exception + assertTrue(false); // never get here + } + + /** + * Clear all properties after all tests. + */ + @AfterAll + public static void after() { + clearAll(); + } + + private static void clearAll() { + System.clearProperty( + "mail.mime.multipart.ignoreexistingboundaryparameter"); + System.clearProperty( + "mail.mime.multipart.ignoremissingboundaryparameter"); + System.clearProperty( + "mail.mime.multipart.ignoremissingendboundary"); + System.clearProperty( + "mail.mime.multipart.allowempty"); + } + + /** + * Create a test message. + * If param is not null, it specifies the boundary parameter. + * The actual boundary is specified by "actual". + * If "end" is true, include the end boundary. + */ + private static MimeMessage createMessage(String param, String actual, + boolean end) throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; " + + (param != null ? "boundary=\"" + param + "\"" : "") + "\n" + + "\n" + + "preamble\n" + + "--" + actual + "\n" + + "\n" + + "first part\n" + + "\n" + + "--" + actual + "\n" + + "\n" + + "second part\n" + + "\n" + + (end ? "--" + actual + "--\n" : ""); + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } + + /** + * Create a test message with no parts. + */ + private static MimeMessage createEmptyMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"x\"\n\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeUtilityTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeUtilityTest.java new file mode 100644 index 0000000..0e809aa --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/MimeUtilityTest.java @@ -0,0 +1,185 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.activation.DataHandler; +import jakarta.activation.DataSource; +import jakarta.activation.FileDataSource; +import jakarta.mail.internet.ContentType; +import jakarta.mail.internet.MimeUtility; +import jakarta.mail.internet.ParseException; +import jakarta.mail.util.ByteArrayDataSource; +import jakarta.mail.util.StreamProvider.EncoderTypes; + +import java.io.File; +import java.util.HashSet; +import java.util.Set; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test MimeUtility methods. + * + * XXX - just a beginning... + * + * @author Bill Shannon + */ +public class MimeUtilityTest { + private static final byte[] utf16beBytes = { + (byte) 0xfe, (byte) 0x5b, (byte) 0xdc, (byte) 0x5f, + (byte) 0x92, (byte) 0x30, (byte) 0x88, (byte) 0x30, + (byte) 0x8d, (byte) 0x30, (byte) 0x57, (byte) 0x30, + (byte) 0x4f, (byte) 0x30, (byte) 0x4a, (byte) 0x30, + (byte) 0x6d, (byte) 0x30, (byte) 0x4c, (byte) 0x30, + (byte) 0x44, (byte) 0x30, (byte) 0x57, (byte) 0x30, + (byte) 0x7e, (byte) 0x30, (byte) 0x59, (byte) 0x30, + (byte) 0x0d, (byte) 0x00, (byte) 0x0a, (byte) 0x00 + }; + + @SuppressWarnings("serial") + private static final Set encodings = new HashSet() {{ + add(EncoderTypes.BIT7_ENCODER.getEncoder()); + add(EncoderTypes.BIT8_ENCODER.getEncoder()); + add(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder()); + add(EncoderTypes.BASE_64.getEncoder()); + }}; + + /** + * Test that utf-16be data is encoded with base64 and not quoted-printable. + */ + @Test + public void testNonAsciiEncoding() throws Exception { + DataSource ds = new ByteArrayDataSource(utf16beBytes, + "text/plain; charset=utf-16be"); + String en = MimeUtility.getEncoding(ds); + assertEquals(EncoderTypes.BASE_64.getEncoder(), en); + } + + /** + * Test that getEncoding returns a valid value even if the file + * doesn't exist. The return value should be a valid + * Content-Transfer-Encoding, but mostly we care that it doesn't + * throw NullPointerException. + */ + @Test + public void getEncodingMissingFile() throws Exception { + File missing = new File(getClass().getName()); + assertFalse(missing.exists()); + FileDataSource fds = new FileDataSource(missing); + assertEquals(fds.getName(), missing.getName()); + assertTrue(encodings.contains(MimeUtility.getEncoding(fds))); + assertTrue(encodings.contains(MimeUtility.getEncoding(new DataHandler(fds)))); + } + + /** + * Test that getEncoding returns a valid value even if the content + * type is bad. The return value should be a valid + * Content-Transfer-Encoding, but mostly we care that it doesn't + * throw NullPointerException. + */ + @Test + public void getEncodingBadContent() throws Exception { + String content = "bad-content-type"; + ContentType type = null; + try { + type = new ContentType(content); + fail(type.toString()); + } catch (ParseException expect) { + if (type != null) { + throw expect; + } + } + + ByteArrayDataSource bads = new ByteArrayDataSource("", content); + bads.setName(null); + assertTrue(encodings.contains(MimeUtility.getEncoding(bads))); + assertTrue(encodings.contains( + MimeUtility.getEncoding(new DataHandler(bads)))); + + bads.setName(""); + assertTrue(encodings.contains(MimeUtility.getEncoding(bads))); + assertTrue(encodings.contains( + MimeUtility.getEncoding(new DataHandler(bads)))); + + bads.setName(getClass().getName()); + assertTrue(encodings.contains(MimeUtility.getEncoding(bads))); + assertTrue(encodings.contains( + MimeUtility.getEncoding(new DataHandler(bads)))); + } + + /** + * Test that encoding a Unicode string with surrogate pairs + * doesn't split the encoding between the pairs. + */ + @Test + public void testSurrogatePairs() throws Exception { + // test a specific case + String sp = "a" + + "\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00" + + "\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00" + + "\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00" + + "\ud801\udc00\ud801\udc00\ud801\udc00\ud801\udc00"; + String en = MimeUtility.encodeText(sp, "utf-8", "B"); + String dt = MimeUtility.decodeText(en); + // encoding it and decoding it shouldn't change it + assertEquals(dt, sp); + String[] w = en.split(" "); + // the first word should end with the second half of a pair + String dw = MimeUtility.decodeWord(w[0]); + assertTrue(dw.charAt(dw.length() - 1) == '\udc00'); + // and the second word should start with the first half of a pair + dw = MimeUtility.decodeWord(w[1]); + assertTrue(dw.charAt(0) == '\ud801'); + + // test various string lengths + int ch = 0xFE000; + String test = ""; + for (int i = 0; i < 50; i++) { + test += new String(Character.toChars(ch)); + String encoded = MimeUtility.encodeText(test, "UTF-8", "B"); + String decoded = MimeUtility.decodeText(encoded); + assertEquals(decoded, test); + } + } + + /** + * Test that encoded words with the wrong Chinese charset still + * decode correctly. + */ + @Test + public void testBadChineseCharsets() throws Exception { + String badgb2312 = "=?gb2312?B?xbfUqqLjIChFVVIpttK7u4EwhDYgKENOWSk=?="; + String badgbk = "=?gbk?B?xbfUqqLjIChFVVIpttK7u4EwhDYgKENOWSk=?="; + String badms936 = "=?ms936?B?xbfUqqLjIChFVVIpttK7u4EwhDYgKENOWSk=?="; + String badcp936 = "=?cp936?B?xbfUqqLjIChFVVIpttK7u4EwhDYgKENOWSk=?="; + String good = "=?gb18030?B?xbfUqqLjIChFVVIpttK7u4EwhDYgKENOWSk=?="; + String goodDecoded = MimeUtility.decodeWord(good); + + assertEquals("gb2312", goodDecoded, MimeUtility.decodeWord(badgb2312)); + assertEquals("gbk", goodDecoded, MimeUtility.decodeWord(badgbk)); + assertEquals("ms936", goodDecoded, MimeUtility.decodeWord(badms936)); + assertEquals("cp936", goodDecoded, MimeUtility.decodeWord(badcp936)); + } + + @Test + public void testLocaleISO885915() throws Exception { + assertEquals("ISO-8859-15", MimeUtility.javaCharset("en_US.iso885915")); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/ModifyMessageTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/ModifyMessageTest.java new file mode 100644 index 0000000..c72b518 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/ModifyMessageTest.java @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.BodyPart; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.StreamProvider.EncoderTypes; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.AsciiStringInputStream; + +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test some of the ways you might modify a message that has been + * read from an input stream. + */ +public class ModifyMessageTest { + + private static Session s = Session.getInstance(new Properties()); + + @Test + public void testAddHeader() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + m.setHeader("a", "b"); + m.saveChanges(); + + MimeMessage m2 = new MimeMessage(m); + assertEquals("b", m2.getHeader("a", null)); + } + + @Test + public void testChangeHeader() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + m.setHeader("Subject", "test"); + m.saveChanges(); + + MimeMessage m2 = new MimeMessage(m); + assertEquals("test", m2.getHeader("Subject", null)); + } + + @Test + public void testAddContent() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText("test"); + mp.addBodyPart(mbp); + m.saveChanges(); + + MimeMessage m2 = new MimeMessage(m); + mp = (MimeMultipart) m2.getContent(); + BodyPart bp = mp.getBodyPart(2); + assertEquals("test", bp.getContent()); + // make sure nothing else changed + bp = mp.getBodyPart(0); + assertEquals("first part\n", bp.getContent()); + bp = mp.getBodyPart(1); + assertEquals("second part\n", bp.getContent()); + } + + @Test + public void testChangeContent() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + BodyPart bp = mp.getBodyPart(0); + bp.setText("test"); + m.saveChanges(); + + MimeMessage m2 = new MimeMessage(m); + mp = (MimeMultipart) m2.getContent(); + bp = mp.getBodyPart(0); + assertEquals("test", bp.getContent()); + } + + @Test + public void testChangeNestedContent() throws Exception { + MimeMessage m = createNestedMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + mp = (MimeMultipart) mp.getBodyPart(0).getContent(); + BodyPart bp = mp.getBodyPart(0); + bp.setText("test"); + m.saveChanges(); + + MimeMessage m2 = new MimeMessage(m); + mp = (MimeMultipart) m2.getContent(); + mp = (MimeMultipart) mp.getBodyPart(0).getContent(); + bp = mp.getBodyPart(0); + assertEquals("test", bp.getContent()); + // make sure other content is not changed or re-encoded + MimeBodyPart mbp = (MimeBodyPart) mp.getBodyPart(1); + assertEquals("second part\n", mbp.getContent()); + assertEquals(EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder(), mbp.getEncoding()); + mbp = (MimeBodyPart) mp.getBodyPart(2); + assertEquals("third part\n", mbp.getContent()); + assertEquals(EncoderTypes.BASE_64.getEncoder(), mbp.getEncoding()); + } + + private static MimeMessage createMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"-\"\n" + + "\n" + + "preamble\n" + + "---\n" + + "\n" + + "first part\n" + + "\n" + + "---\n" + + "\n" + + "second part\n" + + "\n" + + "-----\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } + + private static MimeMessage createNestedMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"-\"\n" + + "\n" + + "preamble\n" + + "---\n" + + "Content-Type: multipart/mixed; boundary=\"x\"\n" + + "\n" + + "--x\n" + + "\n" + + "first part\n" + + "\n" + + "--x\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "\n" + + "second part\n" + + "\n" + + "--x\n" + + "Content-Transfer-Encoding: base64\n" + + "\n" + + "dGhpcmQgcGFydAo=\n" + // "third part\n", base64 encoded + "\n" + + "--x--\n" + + "-----\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/NoEncodeFileNameNoEncodeParametersTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/NoEncodeFileNameNoEncodeParametersTest.java new file mode 100644 index 0000000..1ad03f9 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/NoEncodeFileNameNoEncodeParametersTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2015, 2023 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 org.xbib.net.mail.test.util; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test "mail.mime.encodefilename" System property not set and + * "mail.mime.encodeparameters" set to "false". + */ +public class NoEncodeFileNameNoEncodeParametersTest extends NoEncodeFileNameTest { + + @BeforeAll + public static void before() { + System.out.println("NoEncodeFileNameNoEncodeParameters"); + System.setProperty("mail.mime.charset", "utf-8"); + System.setProperty("mail.mime.encodeparameters", "false"); + // assume mail.mime.encodefilename defaults to false + System.clearProperty("mail.mime.encodefilename"); + } + + @Test + public void test() throws Exception { + MimeBodyPartPublicUpdateHeaders mbp = new MimeBodyPartPublicUpdateHeaders(); + mbp.setText("test"); + mbp.setFileName(fileName); + mbp.updateHeaders(); + String h = mbp.getHeader("Content-Type", ""); + assertTrue(h.contains("name=")); + assertTrue(h.contains(fileName)); + h = mbp.getHeader("Content-Disposition", ""); + assertTrue(h.contains("filename=")); + assertTrue(h.contains(fileName)); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/NoEncodeFileNameTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/NoEncodeFileNameTest.java new file mode 100644 index 0000000..d2900ce --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/NoEncodeFileNameTest.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2015, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.MessagingException; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeUtility; + +import java.io.UnsupportedEncodingException; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test "mail.mime.encodefilename" System property not set. + */ +public class NoEncodeFileNameTest { + + protected static String fileName; + + // a bunch of non-ASCII characters + private static String encodedFileName = + "=?utf-8?B?w4DDgcOFw4bDgMOBw4XDhsOHw4jDicOKw4vDjMONw47Dj8OQw4DD" + + "gcOFw4bDh8OIw4nDisOLw4zDjcOOw4/DkMORw5LDk8OUw5XDlsOYw5nDmsObw5" + + "zDncOew5/DoMOhw6LDo8Okw6XDpsOnw6jDqcOqw6vDrMOtw67Dr8Oww7HDssOz" + + "w7TDtcO2w7jDucO6w7vDvMO9w77Dv8OAw4HDhcOGw4cuZG9j?="; + + static { + try { + fileName = MimeUtility.decodeText(encodedFileName); + } catch (UnsupportedEncodingException ex) { + // should never happen + } + + } + + // RFC 2231 encoding + private static String expected = + "utf-8''%C3%80%C3%81%C3%85%C3%86%C3%80%C3%81%C3%85%C3%86%C3%87%C3%88" + + "%C3%89%C3%8A%C3%8B%C3%8C%C3%8D%C3%8E%C3%8F%C3%90%C3%80%C3%81%C3%85" + + "%C3%86%C3%87%C3%88%C3%89%C3%8A%C3%8B%C3%8C%C3%8D%C3%8E%C3%8F%C3%90" + + "%C3%91%C3%92%C3%93%C3%94%C3%95%C3%96%C3%98%C3%99%C3%9A%C3%9B%C3%9C" + + "%C3%9D%C3%9E%C3%9F%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7" + + "%C3%A8%C3%A9%C3%AA%C3%AB%C3%AC%C3%AD%C3%AE%C3%AF%C3%B0%C3%B1%C3%B2" + + "%C3%B3%C3%B4%C3%B5%C3%B6%C3%B8%C3%B9%C3%BA%C3%BB%C3%BC%C3%BD%C3%BE" + + "%C3%BF%C3%80%C3%81%C3%85%C3%86%C3%87.doc"; + + @BeforeAll + public static void before() { + System.out.println("NoEncodeFileName"); + System.setProperty("mail.mime.charset", "utf-8"); + System.clearProperty("mail.mime.encodefilename"); + } + + @Test + public void test() throws Exception { + MimeBodyPartPublicUpdateHeaders mbp = new MimeBodyPartPublicUpdateHeaders(); + mbp.setText("test"); + mbp.setFileName(fileName); + mbp.updateHeaders(); + String h = mbp.getHeader("Content-Type", ""); + assertTrue(h.contains("name*=")); + assertTrue(h.contains(expected)); + h = mbp.getHeader("Content-Disposition", ""); + assertTrue(h.contains("filename*=")); + assertTrue(h.contains(expected)); + } + + @AfterAll + public static void after() { + // should be unnecessary + System.clearProperty("mail.mime.charset"); + System.clearProperty("mail.mime.encodefilename"); + System.clearProperty("mail.mime.encodeparameters"); + } + + static class MimeBodyPartPublicUpdateHeaders extends MimeBodyPart { + @Override + public void updateHeaders() throws MessagingException { + super.updateHeaders(); + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/NonAsciiBoundaryTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/NonAsciiBoundaryTest.java new file mode 100644 index 0000000..5ae61f6 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/NonAsciiBoundaryTest.java @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.BodyPart; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.AsciiStringInputStream; + +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test that non-ASCII boundary strings are handled reasonably, + * even though, strictly speaking, the MIME spec doesn't allow them. + */ +public class NonAsciiBoundaryTest { + + private static Session s = Session.getInstance(new Properties()); + + @Test + public void test() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + assertEquals(2, mp.getCount()); + BodyPart bp = mp.getBodyPart(0); + assertEquals("first part\n", bp.getContent()); + bp = mp.getBodyPart(1); + assertEquals("second part\n", bp.getContent()); + } + + private static MimeMessage createMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Subject: Example\n" + + "Content-Type: multipart/mixed; boundary=\"\u00A9\"\n" + + "\n" + + "--\u00A9\n" + + "\n" + + "first part\n" + + "\n" + + "--\u00A9\n" + + "\n" + + "second part\n" + + "\n" + + "--\u00A9--\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content, false)); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/ParameterListDecode.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/ParameterListDecode.java new file mode 100644 index 0000000..c409e54 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/ParameterListDecode.java @@ -0,0 +1,259 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.activation.DataHandler; +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.internet.ContentType; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.ParameterList; +import jakarta.mail.internet.ParseException; +import jakarta.mail.util.ByteArrayDataSource; + +import java.io.BufferedReader; +import java.io.ByteArrayOutputStream; +import java.io.InputStreamReader; +import java.io.PrintStream; +import java.util.Enumeration; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test parameter list parsing. + * + * XXX - this should be a JUnit parameterized test, + * but I can't figure out how to run parameterized + * tests under my ClassLoaderSuite. + * + * @author Bill Shannon + */ + +class ParameterListDecode { + static boolean gen_test_input = false; // output good for input to -p + static boolean test_mail = false; // test using a mail server + static int errors = 0; // number of errors detected + static Session session; + static Store store; + static Folder folder; + + static boolean junit; + + protected void testDecode(String paramData) throws Exception { + junit = true; + parse(new BufferedReader(new InputStreamReader( + getClass().getResourceAsStream(paramData)))); + } + + /* + * Parse the input in "mail" format, extracting the Content-Type + * headers and testing them. The parse is rather crude, but sufficient + * to test against most existing UNIX mailboxes. + */ + public static void parse(BufferedReader in) throws Exception { + String header = ""; + + for (; ; ) { + String s = in.readLine(); + if (s != null && !s.isEmpty()) { + char c = s.charAt(0); + if (c == ' ' || c == '\t') { + // a continuation line, add it to the current header + header += '\n' + s; + continue; + } + } + // "s" is the next header, "header" is the last complete header + if (header.regionMatches(true, 0, "Content-Type: ", 0, 14)) { + int i; + String[] expect = null; + if (s != null && s.startsWith("Expect: ")) { + try { + int nexpect = Integer.parseInt(s.substring(8)); + expect = new String[nexpect]; + for (i = 0; i < nexpect; i++) + expect[i] = decode(trim(in.readLine())); + } catch (NumberFormatException e) { + try { + if (s.substring(8, 17).equals("Exception")) { + expect = new String[1]; + expect[0] = "Exception"; + } + } catch (StringIndexOutOfBoundsException se) { + // ignore it + } + } + } + i = header.indexOf(':'); + try { + test(header.substring(0, i), header.substring(i + 2), + expect); + } catch (StringIndexOutOfBoundsException e) { + // ignore + } + } + if (s == null) + return; // EOF + if (s.isEmpty()) { + while ((s = in.readLine()) != null) { + if (s.startsWith("From ")) + break; + } + if (s == null) + return; + } + header = s; + } + } + + /** + * Like String.trim, but only the left side. + */ + public static String trim(String s) { + int i = 0; + while (i < s.length() && s.charAt(i) <= ' ') + i++; + return s.substring(i); + } + + /** + * Decode Unicode escapes. + */ + public static String decode(String s) { + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < s.length(); i++) { + char c = s.charAt(i); + if (c == '\\' && s.charAt(i + 1) == 'u') { + c = (char) Integer.parseInt(s.substring(i + 2, i + 6), 16); + i += 5; + } + sb.append(c); + } + return sb.toString(); + } + + /** + * Test the header's value to see if we can parse it as expected. + */ + public static void test(String header, String value, String[] expect) + throws Exception { + PrintStream out = System.out; + ByteArrayOutputStream bos = null; + if (gen_test_input) { + if (test_mail) { + bos = new ByteArrayOutputStream(); + out = new PrintStream(bos); + } else { + out.println(header + ": " + value); + } + } else if (!junit) + out.println("Test: " + value); + + try { + ContentType ct = new ContentType(value); + ParameterList pl = ct.getParameterList(); + if (gen_test_input) + out.println("Expect: " + pl.size()); + else if (junit) + assertEquals(expect.length, pl.size()); + else { + out.println("Got " + pl.size() + " parameters:"); + if (expect != null && pl.size() != expect.length) { + out.println("Expected " + expect.length + " parameters"); + errors++; + } + } + Enumeration e = pl.getNames(); + for (int i = 0; e.hasMoreElements(); i++) { + String name = e.nextElement(); + String pvalue = pl.get(name); + if (gen_test_input) + out.println("\t" + name + "=" + pvalue); // XXX - newline + else if (junit) { + if (i < expect.length) + assertEquals(expect[i], name + "=" + pvalue); + } else { + out.println("\t[" + (i + 1) + "] Name: " + name + + "\t\tValue: " + pvalue); + if (expect != null && i < expect.length && + !expect[i].equals(name + "=" + pvalue)) { + out.println("\tExpected:\t" + expect[i]); + errors++; + } + } + } + } catch (ParseException e) { + if (gen_test_input) + out.println("Expect: Exception " + e); + else if (junit) + assertTrue(expect.length == 1 && expect[0].equals("Exception")); + else { + out.println("Got Exception: " + e); + if (expect != null && + (expect.length != 1 || !expect[0].equals("Exception"))) { + out.println("Expected " + expect.length + " parameters"); + for (int i = 0; i < expect.length; i++) + out.println("\tExpected:\t" + expect[i]); + errors++; + } + } + } + if (gen_test_input && test_mail) { + MimeMessage msg = new MimeMessage(session); + byte[] buf = bos.toByteArray(); + msg.setDataHandler(new DataHandler( + new ByteArrayDataSource(buf, value))); + msg.saveChanges(); + //msg.writeTo(System.out); + folder.appendMessages(new Message[]{msg}); + } + } + + /** + * Test an individual message. + */ + private static void testMessage(Message msg) throws Exception { + String[] expect = null; + + BufferedReader in = new BufferedReader( + new InputStreamReader(msg.getInputStream())); + + String s = in.readLine(); + if (s != null && s.startsWith("Expect: ")) { + try { + int nexpect = Integer.parseInt(s.substring(8)); + expect = new String[nexpect]; + for (int i = 0; i < nexpect; i++) + expect[i] = trim(in.readLine()); + } catch (NumberFormatException e) { + try { + if (s.substring(8, 17).equals("Exception")) { + expect = new String[1]; + expect[0] = "Exception"; + } + } catch (StringIndexOutOfBoundsException se) { + // ignore it + } + } + } + + String ct = msg.getContentType(); + test("Content-Type: ", ct, expect); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/ParametersNoStrictTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/ParametersNoStrictTest.java new file mode 100644 index 0000000..e811704 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/ParametersNoStrictTest.java @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * Test that the "mail.mime.parameters.strict" System property + * set to false allows bogus parameters to be parsed. + */ +class ParametersNoStrictTest extends ParameterListDecode { + + @BeforeAll + public static void before() { + System.setProperty("mail.mime.parameters.strict", "false"); + } + + @Test + public void testDecode() throws Exception { + testDecode("paramdatanostrict"); + } + + @AfterAll + public static void after() { + // should be unnecessary + System.clearProperty("mail.mime.parameters.strict"); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/PropUtilTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/PropUtilTest.java new file mode 100644 index 0000000..941445a --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/PropUtilTest.java @@ -0,0 +1,208 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.Session; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.util.PropUtil; + +import java.util.HashSet; +import java.util.Properties; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test that the PropUtil methods return the correct values, + * especially when defaults and non-String values are considered. + */ +public class PropUtilTest { + + @Test + public void testInt() throws Exception { + Properties props = new Properties(); + props.setProperty("test", "2"); + assertEquals(PropUtil.getIntProperty(props, "test", 1), 2); + } + + @Test + public void testIntDef() throws Exception { + Properties props = new Properties(); + assertEquals(PropUtil.getIntProperty(props, "test", 1), 1); + } + + @Test + public void testIntDefProp() throws Exception { + Properties defprops = new Properties(); + defprops.setProperty("test", "2"); + Properties props = new Properties(defprops); + assertEquals(PropUtil.getIntProperty(props, "test", 1), 2); + } + + @Test + public void testInteger() throws Exception { + Properties props = new Properties(); + props.put("test", 2); + assertEquals(PropUtil.getIntProperty(props, "test", 1), 2); + } + + @Test + public void testBool() throws Exception { + Properties props = new Properties(); + props.setProperty("test", "true"); + assertTrue(PropUtil.getBooleanProperty(props, "test", false)); + } + + @Test + public void testBoolDef() throws Exception { + Properties props = new Properties(); + assertTrue(PropUtil.getBooleanProperty(props, "test", true)); + } + + @Test + public void testBoolDefProp() throws Exception { + Properties defprops = new Properties(); + defprops.setProperty("test", "true"); + Properties props = new Properties(defprops); + assertTrue(PropUtil.getBooleanProperty(props, "test", false)); + } + + @Test + public void testBoolean() throws Exception { + Properties props = new Properties(); + props.put("test", true); + assertTrue(PropUtil.getBooleanProperty(props, "test", false)); + } + + + // the Session variants... + + @Test + public void testSessionInt() throws Exception { + Properties props = new Properties(); + props.setProperty("test", "2"); + Session sess = Session.getInstance(props, null); + assertEquals(PropUtil.getIntProperty(sess.getProperties(), "test", 1), 2); + } + + @Test + public void testSessionIntDef() throws Exception { + Properties props = new Properties(); + Session sess = Session.getInstance(props, null); + assertEquals(PropUtil.getIntProperty(sess.getProperties(), "test", 1), 1); + } + + @Test + public void testSessionIntDefProp() throws Exception { + Properties defprops = new Properties(); + defprops.setProperty("test", "2"); + Properties props = new Properties(defprops); + Session sess = Session.getInstance(props, null); + assertEquals(PropUtil.getIntProperty(sess.getProperties(), "test", 1), 2); + } + + @Test + public void testSessionInteger() throws Exception { + Properties props = new Properties(); + props.put("test", 2); + Session sess = Session.getInstance(props, null); + assertEquals(PropUtil.getIntProperty(sess.getProperties(), "test", 1), 2); + } + + @Test + public void testSessionBool() throws Exception { + Properties props = new Properties(); + props.setProperty("test", "true"); + Session sess = Session.getInstance(props, null); + assertTrue(PropUtil.getBooleanProperty(sess.getProperties(), "test", false)); + } + + @Test + public void testSessionBoolDef() throws Exception { + Properties props = new Properties(); + Session sess = Session.getInstance(props, null); + assertTrue(PropUtil.getBooleanProperty(sess.getProperties(), "test", true)); + } + + @Test + public void testSessionBoolDefProp() { + Properties defprops = new Properties(); + defprops.setProperty("test", "true"); + Properties props = new Properties(defprops); + Session sess = Session.getInstance(props, null); + assertTrue(PropUtil.getBooleanProperty(sess.getProperties(), "test", false)); + } + + @Test + public void testSessionBoolean() throws Exception { + Properties props = new Properties(); + props.put("test", true); + Session sess = Session.getInstance(props, null); + assertTrue(PropUtil.getBooleanProperty(sess.getProperties(), "test", false)); + } + + + // the System variants... + + @Test + public void testSystemBool() throws Exception { + System.setProperty("test", "true"); + assertTrue(PropUtil.getBooleanSystemProperty("test", false)); + } + + @Test + public void testSystemBoolDef() throws Exception { + assertTrue(PropUtil.getBooleanSystemProperty("testnotset", true)); + } + + @Test + public void testSystemBoolean() throws Exception { + System.getProperties().put("testboolean", true); + assertTrue(PropUtil.getBooleanSystemProperty("testboolean", false)); + } + + @Test + public void testScheduledExecutorWriteTimeout() { + final String executorPropertyName = "test"; + ScheduledExecutorService ses = new ScheduledThreadPoolExecutor(1); + try { + Properties props = new Properties(); + props.put(executorPropertyName, ses); + assertEquals(ses, PropUtil.getScheduledExecutorServiceProperty(props, executorPropertyName)); + } finally { + ses.shutdownNow(); + } + } + + @Test + public void testScheduledExecutorWriteTimeoutIsNull() { + final String executorPropertyName = "test"; + Properties props = new Properties(); + assertNull(PropUtil.getScheduledExecutorServiceProperty(props, executorPropertyName)); + } + + @Test // (expected = ClassCastException.class) + public void testScheduledExecutorWriteTimeoutWrongType() { + final String executorPropertyName = "test"; + Properties props = new Properties(); + props.put(executorPropertyName, new HashSet<>()); + PropUtil.getScheduledExecutorServiceProperty(props, executorPropertyName); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/QPEncoderStreamTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/QPEncoderStreamTest.java new file mode 100644 index 0000000..8648a68 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/QPEncoderStreamTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.util; + +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.util.QPEncoderStream; + +import java.io.ByteArrayOutputStream; +import java.nio.charset.StandardCharsets; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test quoted-printable encoder. + * + * @author Bill Shannon + */ + +public class QPEncoderStreamTest { + /** + * Test that a trailing space is encoded in the output stream. + */ + @Test + public void testTrailingSpace() throws Exception { + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + QPEncoderStream qs = new QPEncoderStream(bos); + qs.write("test ".getBytes(StandardCharsets.US_ASCII)); + qs.flush(); + String result = new String(bos.toByteArray(), StandardCharsets.US_ASCII); + assertEquals("test=20", result); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/ReferencesTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/ReferencesTest.java new file mode 100644 index 0000000..659be39 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/ReferencesTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeMessage; + +import java.util.Properties; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test setting of the References header. + * + * @author Bill Shannon + */ +public class ReferencesTest { + private static Session session = Session.getInstance(new Properties()); + + /* + * Test cases: + * + * Message-Id References In-Reply-To Expected Result + */ + + @Test + public void test1() throws MessagingException { + test(null, null, null, null); + } + + @Test + public void test2() throws MessagingException { + test(null, null, "<1@a>", "<1@a>"); + } + + @Test + public void test3() throws MessagingException { + test(null, "<2@b>", null, "<2@b>"); + } + + @Test + public void test4() throws MessagingException { + test(null, "<2@b>", "<1@a>", "<2@b>"); + } + + @Test + public void test5() throws MessagingException { + test("<3@c>", null, null, "<3@c>"); + } + + @Test + public void test6() throws MessagingException { + test("<3@c>", null, "<1@a>", "<1@a> <3@c>"); + } + + @Test + public void test7() throws MessagingException { + test("<3@c>", "<2@b>", null, "<2@b> <3@c>"); + } + + @Test + public void test8() throws MessagingException { + test("<3@c>", "<2@b>", "<1@a>", "<2@b> <3@c>"); + } + + private static void test(String msgid, String ref, String irt, String res) + throws MessagingException { + MimeMessage msg = new MimeMessage(session); + msg.setFrom(); + msg.setRecipients(Message.RecipientType.TO, "you@example.com"); + msg.setSubject("test"); + if (msgid != null) + msg.setHeader("Message-Id", msgid); + if (ref != null) + msg.setHeader("References", ref); + if (irt != null) + msg.setHeader("In-Reply-To", irt); + msg.setText("text"); + + MimeMessage reply = (MimeMessage) msg.reply(false); + String rref = reply.getHeader("References", " "); + + assertEquals(res, rref); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/RestrictEncodingTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/RestrictEncodingTest.java new file mode 100644 index 0000000..71a9bdc --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/RestrictEncodingTest.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.BodyPart; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.internet.MimeBodyPart; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.internet.MimeMultipart; +import jakarta.mail.util.StreamProvider.EncoderTypes; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.AsciiStringInputStream; + +import java.util.Properties; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Test that the Content-Transfer-Encoding header is ignored + * for composite parts. + * + * XXX - We don't test any of the properties that control this behavior. + */ +public class RestrictEncodingTest { + + private static Session s = Session.getInstance(new Properties()); + + @Test + public void testMultipart() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + assertEquals(2, mp.getCount()); + + BodyPart bp = mp.getBodyPart(0); + assertEquals("first part=\n", bp.getContent()); + } + + @Test + public void testMessage() throws Exception { + MimeMessage m = createMessage(); + MimeMultipart mp = (MimeMultipart) m.getContent(); + + BodyPart bp = mp.getBodyPart(1); + MimeMessage m2 = (MimeMessage) bp.getContent(); + assertEquals("message=\n", m2.getContent()); + } + + @Test + public void testWrite() throws Exception { + MimeMessage m = new MimeMessage(s); + MimeMultipart mp = new MimeMultipart(); + MimeBodyPart mbp = new MimeBodyPart(); + mbp.setText("first part"); + mp.addBodyPart(mbp); + MimeMessage m2 = new MimeMessage(s); + m2.setSubject("example"); + m2.setText("message=\n"); + mbp = new MimeBodyPart(); + mbp.setContent(m2, "message/rfc822"); + mbp.setHeader("Content-Transfer-Encoding", EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder()); + mp.addBodyPart(mbp); + m.setContent(mp); + m.setHeader("Content-Transfer-Encoding", EncoderTypes.QUOTED_PRINTABLE_ENCODER.getEncoder()); + + m = new MimeMessage(m); // copy it + mp = (MimeMultipart) m.getContent(); + + BodyPart bp = mp.getBodyPart(1); + m2 = (MimeMessage) bp.getContent(); + assertEquals("message=\n", m2.getContent()); + } + + private static MimeMessage createMessage() throws MessagingException { + String content = + "Mime-Version: 1.0\n" + + "Content-Type: multipart/mixed; boundary=\"=3D\"\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "\n" + + "--=3D\n" + + "\n" + + "first part=\n" + + "\n" + + "--=3D\n" + + "Content-Type: message/rfc822\n" + + "Content-Transfer-Encoding: quoted-printable\n" + + "\n" + + "Subject: example\n" + + "\n" + + "message=\n" + + "\n" + + "--=3D--\n"; + + return new MimeMessage(s, new AsciiStringInputStream(content)); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/SocketFetcherTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/SocketFetcherTest.java new file mode 100644 index 0000000..585def9 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/SocketFetcherTest.java @@ -0,0 +1,919 @@ +/* + * Copyright (c) 2009, 2024 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 org.xbib.net.mail.test.util; + +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.function.Executable; +import org.xbib.net.mail.test.test.ProtocolHandler; +import org.xbib.net.mail.test.test.TestServer; +import org.xbib.net.mail.util.LineInputStream; +import org.xbib.net.mail.util.MailSSLSocketFactory; +import org.xbib.net.mail.util.SocketFetcher; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.Socket; +import java.nio.charset.StandardCharsets; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.Objects; +import java.util.Properties; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.BiPredicate; +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLHandshakeException; +import javax.net.ssl.SSLSession; +import javax.net.ssl.SSLSocket; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import javax.net.ssl.X509ExtendedTrustManager; +import org.xbib.net.mail.test.imap.IMAPHandler; +import org.xbib.net.mail.test.test.TestSSLSocketFactory; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +/** + * Test SocketFetcher. + */ +@Timeout(20) +public final class SocketFetcherTest { + + /** + * Test connecting with proxy host and port. + */ + @Test + public void testProxyHostPort() { + assertTrue(testProxy("proxy", "localhost", "PPPP")); + } + + /** + * Test connecting with proxy host and port and user name and password. + */ + @Test + public void testProxyHostPortUserPassword() { + assertTrue(testProxyUserPassword("proxy", "localhost", "PPPP", "user", "pwd")); + } + + /** + * Test connecting with proxy host:port. + */ + @Test + public void testProxyHostColonPort() { + assertTrue(testProxy("proxy", "localhost:PPPP", null)); + } + + /** + * Test connecting with socks host and port. + */ + @Test + public void testSocksHostPort() { + assertTrue(testProxy("socks", "localhost", "PPPP")); + } + + /** + * Test connecting with socks host:port. + */ + @Test + public void testSocksHostColonPort() { + assertTrue(testProxy("socks", "localhost:PPPP", null)); + } + + /** + * Test connecting with no proxy. + */ + @Test + public void testNoProxy() { + assertFalse(testProxy("none", "localhost", null)); + } + + /** + * HTTP response and IMAP response together. + * This test verifies the IMAP response will not be read when reading the proxy response. + */ + @Test + public void issue45Success() throws IOException { + String imapResponse = "* OK NAME IMAP4rev1 Server Server 1ece50b148c8 is ready."; + StringBuilder message = new StringBuilder(); + message.append("HTTP/1.0 200 Connection established\r\n"); + message.append("More things\r\n"); + message.append("\r\n"); + message.append(imapResponse).append("\r\n"); + InputStream proxyResponse = new ByteArrayInputStream(message.toString().getBytes(StandardCharsets.UTF_8)); + assertTrue(SocketFetcher.readProxyResponse(proxyResponse, new StringBuilder())); + LineInputStream r = new LineInputStream(proxyResponse, true); + /* IMAP response was not read yet. + * Next line would fail if SocketFetcher.readProxyResponse uses a BufferedInputStream + * because all the input is read and buffered. + */ + assertEquals(imapResponse, r.readLine()); + } + + @Test + public void issue45Failure() throws IOException { + String errorMessage = "HTTP/1.0 403 Error"; + StringBuilder message = new StringBuilder(); + message.append(errorMessage).append("\r\n"); + message.append("More things\r\n"); + message.append("\r\n"); + InputStream proxyResponse = new ByteArrayInputStream(message.toString().getBytes(StandardCharsets.UTF_8)); + StringBuilder error = new StringBuilder(); + assertFalse(SocketFetcher.readProxyResponse(proxyResponse, error)); + assertEquals(errorMessage, error.toString()); + } + + @Test + public void testSSLHostnameVerifierAcceptsConnections() throws Exception { + testSSLHostnameVerifier(true); + } + + /** + * Test connecting (IMAP) with SSL using a custom hostname verifier which will + * reject all connections. + * + * @throws Exception + */ + @Test + public void testSSLHostnameVerifierRejectsConnections() throws Exception { + testSSLHostnameVerifier(false); + } + + @Test + public void testSSLVerifierInstantiatedByString() throws Exception { + testSSLHostnameVerifierByName(); + } + + /** + * Utility method for testing a custom {@link HostnameVerifier}. + * + * @param acceptConnections Whether the {@link HostnameVerifier} should accept or reject connections. + * @throws Exception + */ + private void testSSLHostnameVerifier(boolean acceptConnections) throws Exception { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.ssl.enable", "true"); + + TestSSLSocketFactory sf = new TestSSLSocketFactory(); + properties.put("mail.imap.ssl.socketFactory", sf); + + // don't fall back to non-SSL + properties.setProperty("mail.imap.socketFactory.fallback", "false"); + + TestHostnameVerifier hnv = new TestHostnameVerifier(acceptConnections); + properties.put("mail.imap.ssl.hostnameverifier", hnv); + properties.setProperty("mail.imap.ssl.checkserveridentity", "false"); + + Executable runnable = () -> { + TestServer server = null; + try { + server = new TestServer(new IMAPHandler(), true); + server.start(); + + properties.setProperty("mail.imap.port", + Integer.toString(server.getPort())); + final Session session = Session.getInstance(properties); + + try (Store store = session.getStore("imap")) { + store.connect("test", "test"); + } + } finally { + if (server != null) { + server.quit(); + } + } + }; + + if (!acceptConnections) { + // When the hostname verifier refuses a connection, a MessagingException will be thrown. + assertNotNull(assertThrows(MessagingException.class, runnable)); + } else { + // When the hostname verifier is not set to refuse connections, no exception should be thrown. + synchronized (TestHostnameVerifier.class) { + try { + runnable.execute(); + } catch (Throwable t){ + throw new AssertionError(t); + } finally { + TestHostnameVerifier.reset(); + } + } + } + + // Ensure the custom hostname verifier was actually used. + assertTrue(hnv.hasInvokedVerify()); + } + + private void testSSLHostnameVerifierByName() throws Exception { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.ssl.enable", "true"); + + TestSSLSocketFactory sf = new TestSSLSocketFactory(); + properties.put("mail.imap.ssl.socketFactory", sf); + + // don't fall back to non-SSL + properties.setProperty("mail.imap.socketFactory.fallback", "false"); + + properties.setProperty("mail.imap.ssl.hostnameverifier.class", TestHostnameVerifier.class.getName()); + properties.setProperty("mail.imap.ssl.checkserveridentity", "false"); + + Executable runnable = () -> { + TestServer server = null; + try { + server = new TestServer(new IMAPHandler(), true); + server.start(); + + properties.setProperty("mail.imap.port", + Integer.toString(server.getPort())); + final Session session = Session.getInstance(properties); + + try (Store store = session.getStore("imap")) { + store.connect("test", "test"); + } + } finally { + if (server != null) { + server.quit(); + } + } + }; + + synchronized (TestHostnameVerifier.class) { + try { + runnable.execute(); + assertEquals(1, TestHostnameVerifier.getDefaultConstructorCount()); + } catch (Throwable t) { + throw new AssertionError(t); + } finally { + TestHostnameVerifier.reset(); + } + } + } + + @Test + public void testSSLHostnameVerifierHostNameCheckerFQCN() throws Exception { + try { + testSSLHostnameVerifierClass("localhost", "sun.security.util.HostnameChecker"); + throw new AssertionError("No exception"); + } catch (MessagingException me) { + Throwable cause = me.getCause(); + assertTrue(cause instanceof IOException); + assertTrue(isFromSocketFetcher(me)); + assumeTrue(isSunSecurityOpen(cause)); + } + } + + @Test + public void testSSLHostnameVerifierHostNameChecker() throws Exception { + try { + testSSLHostnameVerifierClass("localhost", "JdkHostnameChecker"); + throw new AssertionError("No exception"); + } catch (MessagingException me) { + Throwable cause = me.getCause(); + assertTrue(cause instanceof IOException); + assertTrue(isFromSocketFetcher(me)); + assumeTrue(isSunSecurityOpen(cause)); + } + } + + @Test + public void testSSLHostnameVerifierHostNameCheckerIPv4() throws Exception { + try { + testSSLHostnameVerifierClass("127.0.0.1", "JdkHostnameChecker"); + throw new AssertionError("No exception"); + } catch (MessagingException me) { + Throwable cause = me.getCause(); + assertTrue(cause instanceof IOException); + assertTrue(isFromSocketFetcher(me)); + assumeTrue(isSunSecurityOpen(cause)); + } + } + + @Test + public void testSSLHostnameVerifierHostNameCheckerIPv6() throws Exception { + try { + testSSLHostnameVerifierClass("::1", "JdkHostnameChecker"); + throw new AssertionError("No exception"); + } catch (MessagingException me) { + Throwable cause = me.getCause(); + assertTrue(cause instanceof IOException); + assertTrue(isFromSocketFetcher(me)); + assumeTrue(isSunSecurityOpen(cause)); + } + } + + @Test + public void testSSLHostnameVerifierClassCastException() { + try { + testSSLHostnameVerifierClass("localhost", String.class.getName()); + throw new AssertionError("No exception"); + } catch (MessagingException me) { + assertTrue(matchAnyCauseStackTrace(me, + (t, s) -> t instanceof ClassCastException)); + } catch (Exception e) { + throw new AssertionError(e); + } + } + + @Test + public void testSSLHostnameVerifierRemovedAlias() { + //Reserve all identifiers that don't contain a package for future use by + //Angus Mail. Ensure removed aliases never fallback to classloading as + //that might find a malicious class of the same name as the alias. + try { + testSSLHostnameVerifierClass("localhost", "foobarbaz"); + throw new AssertionError("No exception"); + } catch (MessagingException me) { + assertFalse(isFromClassLoading(me)); + } catch(Exception e) { + throw new AssertionError(e); + } + } + + @Test + public void testSSLHostnameVerifierLegacy() throws Exception { + try { + testSSLHostnameVerifierClass("localhost", "legacy"); + throw new AssertionError("No exception"); + } catch (MessagingException me) { + Throwable cause = me.getCause(); + assertTrue(cause instanceof IOException); + assertTrue(isFromSocketFetcher(me)); + } + } + + @Test + public void testSSLHostnameVerifierMail() throws Exception { + try { + testSSLHostnameVerifierClass("localhost", "MailHostnameVerifier"); + throw new AssertionError("No exception"); + } catch (MessagingException me) { + Throwable cause = me.getCause(); + assertTrue(cause instanceof IOException); + assertTrue(isFromSocketFetcher(me)); + } + } + + private void testSSLHostnameVerifierClass(String host, String name) throws Exception { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", host); + properties.setProperty("mail.imap.ssl.enable", "true"); + + TestSSLSocketFactory sf = new TestSSLSocketFactory(); + properties.put("mail.imap.ssl.socketFactory", sf); + + // don't fall back to non-SSL + properties.setProperty("mail.imap.socketFactory.fallback", "false"); + + properties.setProperty("mail.imap.ssl.hostnameverifier.class", name); + properties.setProperty("mail.imap.ssl.checkserveridentity", "false"); + + TestServer server = null; + try { + server = new TestServer(new IMAPHandler(), true); + server.start(); + + properties.setProperty("mail.imap.port", + Integer.toString(server.getPort())); + final Session session = Session.getInstance(properties); + + try (Store store = session.getStore("imap")) { + store.connect("test", "test"); + } + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * Endpoint identity check is enforced when TrustManager type is not + * an X509ExtendedTrustManager which is compatible legacy behavior. + */ + @Test + public void testSSLCheckServerIdentityTrustManager() { + try { + testSSLCheckServerIdentity( + new AllowAllX509TrustManager(), (String) null); + throw new AssertionError(); + } catch (Error | RuntimeException e) { + throw e; + } catch (MessagingException me) { + Throwable cause = me.getCause(); + assertTrue(cause instanceof SSLHandshakeException); + assertTrue(isFromTrustManager(me)); + } catch (Exception t) { + throw new AssertionError(t); + } + } + + /** + * Endpoint identity check is not enforced when TrustManager type is an + * X509ExtendedTrustManager. This is not compatible with legacy behavior. + * Custom X509ExtendedTrustManager implementation should inspect the given + */ + @Test + public void testSSLCheckServerIdentityExtendedTrustManager() throws Exception { + testSSLCheckServerIdentity( + new AllowAllX509ExtendedTrustManager(), (String) null); + } + + @Test + public void testSSLCheckCompatibility() { + testSSLCheckCompatibility("MailHostnameVerifier"); + } + + @Test + public void testSSLCheckCompatibilityStrict() { + testSSLCheckCompatibility("legacy"); + } + + private void testSSLCheckCompatibility(String hnv) { + try { + testSSLCheckServerIdentity( + new AllowAllX509ExtendedTrustManager(), hnv); + throw new AssertionError(); + } catch (Error | RuntimeException e) { + throw e; + } catch (MessagingException me) { + assertTrue(isFromSocketFetcher(me)); + assertFalse(isFromTrustManager(me)); + if (!matchAnyCauseStackTrace(me, (t, s) -> + "verify".equals(s.getMethodName()) + && (s.getClassName().contains("MailHostnameVerifier") + || s.getClassName().contains("JdkHostnameChecker")))) { + throw new AssertionError(me); + } + } catch (Exception t) { + throw new AssertionError(t); + } + } + + private void testSSLCheckServerIdentity(TrustManager tm, String hnv) throws Exception { + final Properties props = new Properties(); + props.setProperty("mail.imap.host", "localhost"); + props.setProperty("mail.imap.ssl.enable", "true"); + + MailSSLSocketFactory sf = new MailSSLSocketFactory(); + sf.setTrustedHosts("localhost"); + sf.setTrustAllHosts(true); + sf.setTrustManagers(tm); + props.put("mail.imap.ssl.socketFactory", sf); + + // don't fall back to non-SSL + props.setProperty("mail.imap.socketFactory.fallback", "false"); + props.setProperty("mail.imap.ssl.checkserveridentity", "true"); + + if (hnv != null) { + props.setProperty("mail.imap.ssl.hostnameverifier.class", hnv); + } + + TestServer server = null; + try { + server = new TestServer(new IMAPHandler(), true); + server.start(); + + props.setProperty("mail.imap.port", + Integer.toString(server.getPort())); + final Session session = Session.getInstance(props); + + try (Store store = session.getStore("imap")) { + store.connect("test", "test"); + } + } finally { + if (server != null) { + server.quit(); + } + } + } + + @Test + public void testSSLCheckServerIdentityFalse() throws Throwable { + testSSLCheckServerIdentity("localhost", "false"); + } + + @Test + public void testSSLCheckServerIdentityNull() { + try { + testSSLCheckServerIdentity("localhost", (String) null); + throw new AssertionError(); + } catch (Error | RuntimeException e) { + throw e; + } catch (MessagingException me) { + Throwable cause = me.getCause(); + assertTrue(cause instanceof SSLHandshakeException); + assertTrue(isFromTrustManager(me)); + } catch (Throwable t) { + throw new AssertionError(t); + } + } + + @Test + public void testSSLCheckServerIdentityTrue() { + try { + testSSLCheckServerIdentity("localhost", "true"); + throw new AssertionError(); + } catch (Error | RuntimeException e) { + throw e; + } catch (MessagingException me) { + Throwable cause = me.getCause(); + assertTrue(cause instanceof SSLHandshakeException); + assertTrue(isFromTrustManager(me)); + } catch (Throwable t) { + throw new AssertionError(t); + } + } + + @Test + public void testSSLCheckServerIdentityIPv4True() { + try { + testSSLCheckServerIdentity("127.0.0.1", "true"); + throw new AssertionError(); + } catch (Error | RuntimeException e) { + throw e; + } catch (MessagingException me) { + Throwable cause = me.getCause(); + assertTrue(cause instanceof SSLHandshakeException); + assertTrue(isFromTrustManager(me)); + } catch (Throwable t) { + throw new AssertionError(t); + } + } + + @Test + public void testSSLCheckServerIdentityIPv6True() { + try { + testSSLCheckServerIdentity("::1", "true"); + throw new AssertionError(); + } catch (Error | RuntimeException e) { + throw e; + } catch (MessagingException me) { + Throwable cause = me.getCause(); + assertTrue(cause instanceof SSLHandshakeException); + assertTrue(isFromTrustManager(me)); + } catch (Throwable t) { + throw new AssertionError(t); + } + } + + private boolean matchAnyCauseStackTrace(Throwable thrown, + BiPredicate matcher) { + Objects.requireNonNull(matcher); + for (Throwable t = thrown; t != null; t = t.getCause()) { + for (StackTraceElement s : t.getStackTrace()) { + if (matcher.test(t, s)) { + return true; + } + } + } + return false; + } + + private boolean isFromTrustManager(Throwable thrown) { + return matchAnyCauseStackTrace(thrown, (t, s) -> + "checkServerTrusted".equals(s.getMethodName()) + && s.getClassName().contains("TrustManager")); + } + + private boolean isSunSecurityOpen(Throwable thrown) { + return !matchAnyCauseStackTrace(thrown, (t, s) -> + t instanceof IllegalAccessException + && "verify".equals(s.getMethodName()) + && s.getClassName().contains("JdkHostnameChecker")); + } + + private boolean isFromSocketFetcher(Throwable thrown) { + return matchAnyCauseStackTrace(thrown, (t, s) -> + "checkServerIdentity".equals(s.getMethodName()) + && SocketFetcher.class.getName().equals(s.getClassName())); + } + + private boolean isFromClassLoading(Throwable thrown) { + return matchAnyCauseStackTrace(thrown, (t, s) -> + t instanceof ClassNotFoundException + && ("forName".equals(s.getMethodName()) + || "loadClass".equals(s.getMethodName()))); + } + + + private void testSSLCheckServerIdentity(String host, String check) throws Throwable { + final Properties props = new Properties(); + props.setProperty("mail.imap.host", host); + props.setProperty("mail.imap.ssl.enable", "true"); + + TestSSLSocketFactory sf = new TestSSLSocketFactory(); + props.put("mail.imap.ssl.socketFactory", sf); + + // don't fall back to non-SSL + props.setProperty("mail.imap.socketFactory.fallback", "false"); + + if (check != null) { + props.setProperty("mail.imap.ssl.checkserveridentity", check); + } + + TestServer server = null; + try { + server = new TestServer(new IMAPHandler(), true); + server.start(); + + props.setProperty("mail.imap.port", + Integer.toString(server.getPort())); + final Session session = Session.getInstance(props); + + try (Store store = session.getStore("imap")) { + store.connect("test", "test"); + } + } finally { + if (server != null) { + server.quit(); + } + } + } + + /** + * + */ + public boolean testProxy(String type, String host, String port) { + return testProxyUserPassword(type, host, port, null, null); + } + + /** + * + */ + public boolean testProxyUserPassword(String type, String host, String port, + String user, String pwd) { + TestServer server = null; + try { + ProxyHandler handler = new ProxyHandler(type.equals("proxy")); + server = new TestServer(handler); + server.start(); + String sport = String.valueOf(server.getPort()); + + //System.setProperty("mail.socket.debug", "true"); + Properties properties = new Properties(); + properties.setProperty("mail.test.host", "localhost"); + properties.setProperty("mail.test.port", "2"); + properties.setProperty("mail.test." + type + ".host", + host.replace("PPPP", sport)); + if (port != null) + properties.setProperty("mail.test." + type + ".port", + port.replace("PPPP", sport)); + if (user != null) + properties.setProperty("mail.test." + type + ".user", user); + if (pwd != null) + properties.setProperty("mail.test." + type + ".password", pwd); + + Socket s = null; + try { + s = SocketFetcher.getSocket("localhost", 2, + properties, "mail.test", false); + } catch (Exception ex) { + // ignore failure, which is expected + //System.out.println(ex); + //ex.printStackTrace(); + } finally { + if (s != null) + s.close(); + } + if (!handler.getConnected()) + return false; + if (user != null && pwd != null) + return (user + ":" + pwd).equals(handler.getUserPassword()); + else + return true; + + } catch (final Exception e) { + //e.printStackTrace(); + fail(e.getMessage()); + return false; // XXX - doesn't matter + } finally { + if (server != null) { + server.quit(); + } + } + } + + private static final class AllowAllX509TrustManager + implements X509TrustManager { + + AllowAllX509TrustManager() { + } + + @Override + public void checkClientTrusted(X509Certificate[] xcs, String string) + throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] xcs, String string) + throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + + private static final class AllowAllX509ExtendedTrustManager + extends X509ExtendedTrustManager { + + private static final String ALGO = "LDAPS"; + + AllowAllX509ExtendedTrustManager() { + } + + @Override + public void checkClientTrusted(X509Certificate[] xcs, String string, + Socket socket) throws CertificateException { + checkServerTrusted(xcs, string, socket); + } + + @Override + public void checkServerTrusted(X509Certificate[] xcs, String string, + Socket socket) throws CertificateException { + if (socket == null) { + throw new CertificateException("Null socket"); + } + + if (socket.isClosed()) { + throw new CertificateException("closed"); + } + + if (!socket.isConnected()) { + throw new CertificateException("not connected"); + } + + //Check that .ssl.checkserveridentity=true + final String eia = ((SSLSocket) socket) + .getSSLParameters().getEndpointIdentificationAlgorithm(); + if (!ALGO.equals(eia)) { + throw new CertificateException(eia); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] xcs, String string, + SSLEngine ssle) throws CertificateException { + checkServerTrusted(xcs, string, ssle); + } + + @Override + public void checkServerTrusted(X509Certificate[] xcs, String string, + SSLEngine ssle) throws CertificateException { + if (ssle == null) { + throw new CertificateException("Null engine"); + } + + //Check that .ssl.checkserveridentity=true + final String eia = ssle.getSSLParameters() + .getEndpointIdentificationAlgorithm(); + if (!ALGO.equals(eia)) { + throw new CertificateException(eia); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] xcs, String string) + throws CertificateException { + } + + @Override + public void checkServerTrusted(X509Certificate[] xcs, String string) + throws CertificateException { + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return null; + } + } + + public static class TestHostnameVerifier implements HostnameVerifier { + /* + * This is based on an assumption that the hostname verifier is instantiated + * by its default constructor in a managed way. + * + * Unit tests that check this property should impose their own thread safety. + * For example, when executing code expected to be using the TestHostnameVerifier, + * the unit test may synchronize on the TestHostnameVerifier class and call the + * static "reset" method prior to de-synchronizing. + */ + private static final AtomicInteger defaultConstructorCount = new AtomicInteger(); + private boolean acceptConnections = true; + private boolean verified = false; + + public TestHostnameVerifier() { + defaultConstructorCount.getAndIncrement(); + } + + public TestHostnameVerifier(boolean acceptConnections) { + this.acceptConnections = acceptConnections; + } + + @Override + public boolean verify(String hostname, SSLSession session) { + verified = true; + return acceptConnections; + } + + /** + * Indicates whether the hostname verifier has been used. + * @return true if + */ + public boolean hasInvokedVerify() { + return verified; + } + + public static int getDefaultConstructorCount() { + return defaultConstructorCount.get(); + } + + /** + * Used to reset static values. + */ + public static void reset() { + defaultConstructorCount.set(0); + } + } + + /** + * Custom handler. Remember whether any data was sent + * and save user/password string; + */ + private static class ProxyHandler extends ProtocolHandler { + private final boolean http; + + // must be static because handler is cloned for each connection + private static volatile boolean connected; + private static volatile String userPassword; + + public ProxyHandler(boolean http) { + this.http = http; + connected = false; + } + + @Override + public void handleCommand() throws IOException { + if (!http) { + int c = in.read(); + if (c >= 0) { + // any data means a real client connected + connected = true; + } + exit(); + } + + // else, http... + String line; + while ((line = readLine()) != null) { + // any data means a real client connected + connected = true; + if (line.length() == 0) + break; + if (line.startsWith("Proxy-Authorization:")) { + int i = line.indexOf("Basic ") + 6; + String up = line.substring(i); + userPassword = new String(Base64.getDecoder().decode( + up.getBytes(StandardCharsets.US_ASCII)), + StandardCharsets.UTF_8); + } + } + exit(); + } + + public boolean getConnected() { + return connected; + } + + public String getUserPassword() { + return userPassword; + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/TimeoutOutputStreamTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/TimeoutOutputStreamTest.java new file mode 100644 index 0000000..5e08772 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/TimeoutOutputStreamTest.java @@ -0,0 +1,532 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.util; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.xbib.net.mail.test.test.ReflectionUtil; +import org.xbib.net.mail.util.TimeoutOutputStream; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.ServerSocket; +import java.net.Socket; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.Delayed; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class TimeoutOutputStreamTest { + private List serverSockets = new ArrayList<>(); + private List sockets = new ArrayList<>(); + private List scheduledExecutorServices = new ArrayList<>(); + + @AfterEach + public void tearDown() { + scheduledExecutorServices.forEach(this::close); + scheduledExecutorServices.clear(); + serverSockets.forEach(this::close); + serverSockets.clear(); + sockets.forEach(this::close); + sockets.clear(); + } + + @Test + public void testWriteSesWithRemoveOnCancelPolicyTrue() throws Exception { + Socket socket = createSocket(); + ScheduledThreadPoolExecutor ses = createSes(true); + BlockingQueue queue = ses.getQueue(); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 5000); + + timeoutOutputStream.write(new byte[]{0}, 0, 1); + + assertEquals(0, queue.size()); + assertTrue(getScheduledFeature(timeoutOutputStream).isCancelled()); + } + + @Test + public void testWriteSesWithRemoveOnCancelPolicyFalse() throws Exception { + Socket socket = createSocket(); + ScheduledThreadPoolExecutor ses = createSes(); + BlockingQueue queue = ses.getQueue(); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 5000); + + timeoutOutputStream.write(new byte[]{0}, 0, 1); + + assertEquals(1, queue.size()); + ScheduledFuture sf = (ScheduledFuture) queue.peek(); + assertTrue(sf.isCancelled()); + } + + @Test + public void testWriteSesWithRemoveOnCancelPolicyFalseWithoutTimeout() throws Exception { + Socket socket = createSocket(); + ScheduledThreadPoolExecutor ses = createSes(); + BlockingQueue queue = ses.getQueue(); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 0); + + timeoutOutputStream.write(new byte[]{0}, 0, 1); + + assertEquals(0, queue.size()); + assertNull(getScheduledFeature(timeoutOutputStream)); + } + + @Test + public void testWriteRejectedExecutionException() throws Exception { + Socket socket = createSocket(); + ScheduledThreadPoolExecutor ses = createSes(); + ses.shutdownNow(); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 1); + + IOException expectedException = + assertThrows(IOException.class, () -> timeoutOutputStream.write(new byte[]{0}, 0, 1)); + + assertEquals("Write aborted due to timeout not enforced", expectedException.getMessage()); + assertFalse(socket.isClosed()); + } + + @Test + public void testWriteSwallowRejectedExecutionException() throws Exception { + Socket socket = createSocket(); + ScheduledThreadPoolExecutor ses = createSes(); + ses.shutdownNow(); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 1); + socket.close(); + + IOException expectedException = + assertThrows(IOException.class, () -> timeoutOutputStream.write(new byte[]{0}, 0, 1)); + + assertEquals("Socket closed", expectedException.getMessage()); + assertTrue(socket.isClosed()); + } + + @Test + public void testSocketClosedAfterWrite() throws Exception { + Socket socket = createSocket(); + ScheduledThreadPoolExecutor ses = createSes(); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 0); + + timeoutOutputStream.write(new byte[]{0}, 0, 1); + + assertFalse(socket.isClosed()); + assertNull(getScheduledFeature(timeoutOutputStream)); + } + + @Test + public void testWriteSocketNoTimeoutWithIOException() throws Exception { + Socket socket = createSocket(); + ScheduledThreadPoolExecutor ses = createSes(); + TimeoutOutputStream timeoutOutputStream = + new TimeoutOutputStream(socket, ses, 0); + ReflectionUtil.setFieldValue(timeoutOutputStream, "os", + getOutputStreamIOException(socket.getOutputStream(), Duration.ofSeconds(0), new IOException("an error"))); + + IOException expectedException = assertThrows(IOException.class, + () -> timeoutOutputStream.write(new byte[]{1}, 0, 1)); + + assertEquals("an error", expectedException.getMessage()); + } + + @Test + public void testWriteSocketClosedByWriteTimeout() throws Exception { + Socket socket = createSocket(); + ScheduledThreadPoolExecutor ses = createSes(); + TimeoutOutputStream timeoutOutputStream = + new TimeoutOutputStream(socket, ses, 1); + ReflectionUtil.setFieldValue(timeoutOutputStream, "os", + getSlowOutputStream(socket.getOutputStream(), Duration.ofSeconds(1), null)); + + IOException expectedException = assertThrows(IOException.class, + () -> timeoutOutputStream.write(new byte[]{1}, 0, 1)); + + assertEquals("Write timed out", expectedException.getMessage()); + assertTrue(socket.isClosed()); + } + + @Test + public void testWriteSocketClosedByWriteTimeoutWithException() throws Exception { + Socket socket = createSocket(); + ScheduledThreadPoolExecutor ses = createSes(); + TimeoutOutputStream timeoutOutputStream = + new TimeoutOutputStream(socket, ses, 1); + ReflectionUtil.setFieldValue(timeoutOutputStream, "os", + getSlowOutputStream(socket.getOutputStream(), Duration.ofSeconds(1), new RuntimeException("Unknown error"))); + + IOException expectedException = assertThrows(IOException.class, + () -> timeoutOutputStream.write(new byte[]{1}, 0, 1)); + + assertEquals("java.lang.RuntimeException: Unknown error", expectedException.getMessage()); + assertTrue(socket.isClosed()); + } + + @Test + public void testHandleTimeoutTaskResultCancellationException() throws Exception { + Socket socket = createSocket(); + CustomScheduledThreadPoolExecutor ses = new CustomScheduledThreadPoolExecutor(1); + this.scheduledExecutorServices.add(ses); + CancellationException e = new CancellationException("An exception happened"); + ses.setCancellationException(e); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 1); + ReflectionUtil.setFieldValue(timeoutOutputStream, "os", + getOutputStreamIOException(socket.getOutputStream(), Duration.ofSeconds(1), new IOException("any error"))); + + IOException exception = + assertThrows(IOException.class, () -> timeoutOutputStream.write(new byte[]{0}, 0, 1)); + + assertEquals("java.io.IOException: Couldn't get result of timeout task. java.util.concurrent" + + ".CancellationException: An exception happened", exception.toString()); + assertEquals("java.io.IOException: any error", exception.getCause().toString()); + } + + @Test + public void testHandleTimeoutTaskResultTimeoutException() throws Exception { + Socket socket = createSocket(); + CustomScheduledThreadPoolExecutor ses = new CustomScheduledThreadPoolExecutor(1); + this.scheduledExecutorServices.add(ses); + TimeoutException e = new TimeoutException("An exception happened"); + ses.setTimeoutException(e); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 1); + ReflectionUtil.setFieldValue(timeoutOutputStream, "os", + getOutputStreamIOException(socket.getOutputStream(), Duration.ofSeconds(1), new IOException("any error"))); + + IOException exception = + assertThrows(IOException.class, () -> timeoutOutputStream.write(new byte[]{0}, 0, 1)); + + assertTrue(exception.toString().startsWith( + "java.io.IOException: Couldn't get result of timeout task. java" + + ".util.concurrent.TimeoutException: An exception happened")); + assertTrue(exception.toString().contains("CustomScheduledThreadPoolExecutor")); + assertEquals("java.io.IOException: any error", exception.getCause().toString()); + } + + @Test + public void testHandleTimeoutTaskResultExecutionException() throws Exception { + Socket socket = createSocket(); + CustomScheduledThreadPoolExecutor ses = new CustomScheduledThreadPoolExecutor(1); + this.scheduledExecutorServices.add(ses); + ExecutionException e = new ExecutionException(new RuntimeException("Random exception")); + ses.setExecutionException(e); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 1); + ReflectionUtil.setFieldValue(timeoutOutputStream, "os", + getOutputStreamIOException(socket.getOutputStream(), Duration.ofSeconds(1), new IOException("any error"))); + + IOException exception = + assertThrows(IOException.class, () -> timeoutOutputStream.write(new byte[]{0}, 0, 1)); + + assertEquals("java.io.IOException: Couldn't get result of timeout task." + + " java.lang.RuntimeException: Random exception", exception.toString()); + assertEquals("java.io.IOException: any error", exception.getCause().toString()); + } + + @Test + public void testHandleTimeoutTaskResultInterruptedException() throws Exception { + Socket socket = createSocket(); + CustomScheduledThreadPoolExecutor ses = new CustomScheduledThreadPoolExecutor(1); + this.scheduledExecutorServices.add(ses); + InterruptedException e = new InterruptedException("An exception happened"); + ses.setInterruptedException(e); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 1); + ReflectionUtil.setFieldValue(timeoutOutputStream, "os", + getOutputStreamIOException(socket.getOutputStream(), Duration.ofSeconds(1), new IOException("any error"))); + + IOException exception = + assertThrows(IOException.class, () -> timeoutOutputStream.write(new byte[]{0}, 0, 1)); + + assertEquals("java.io.IOException: Couldn't get result of timeout task. " + + "java.lang.InterruptedException: An exception happened", exception.toString()); + assertEquals("java.io.IOException: any error", exception.getCause().toString()); + } + + @Test + public void testHandleTimeoutTaskResultRuntimeException() throws Exception { + Socket socket = createSocket(); + CustomScheduledThreadPoolExecutor ses = new CustomScheduledThreadPoolExecutor(1); + this.scheduledExecutorServices.add(ses); + RuntimeException e = new RuntimeException("An exception happened"); + ses.setRuntimeException(e); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 1); + ReflectionUtil.setFieldValue(timeoutOutputStream, "os", + getOutputStreamIOException(socket.getOutputStream(), Duration.ofSeconds(1), new IOException("any error"))); + + IOException exception = + assertThrows(IOException.class, () -> timeoutOutputStream.write(new byte[]{0}, 0, 1)); + + assertEquals("java.io.IOException: Couldn't get result of timeout task. " + + "java.lang.RuntimeException: An exception happened", exception.toString()); + assertEquals("java.io.IOException: any error", exception.getCause().toString()); + } + + @Test + public void testHandleTimeoutTaskResultWithNoException() throws Exception { + Socket socket = createSocket(); + CustomScheduledThreadPoolExecutor ses = new CustomScheduledThreadPoolExecutor(1); + this.scheduledExecutorServices.add(ses); + TimeoutOutputStream timeoutOutputStream = new TimeoutOutputStream(socket, ses, 1); + ReflectionUtil.setFieldValue(timeoutOutputStream, "os", + getOutputStreamIOException(socket.getOutputStream(), Duration.ofSeconds(1), new IOException("any error"))); + + IOException exception = + assertThrows(IOException.class, () -> timeoutOutputStream.write(new byte[]{0}, 0, 1)); + + assertEquals("java.io.IOException: some result", exception.toString()); + } + + private int getRandomFreePort() throws IOException { + ServerSocket serverSocket = new ServerSocket(0); + serverSockets.add(serverSocket); + int freePort = serverSocket.getLocalPort(); + + return freePort; + } + + private OutputStream getSlowOutputStream(OutputStream os, Duration delay, RuntimeException exception) { + OutputStream outputStream = new OutputStream() { + @Override + public void write(int i) throws IOException { + try { + Thread.sleep(delay.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new IOException(e); + } + os.write(i); + } + + @Override + public void close() throws IOException { + os.close(); + if (exception != null) { + throw exception; + } + } + }; + + return outputStream; + } + + private OutputStream getOutputStreamIOException(OutputStream os, Duration delay, IOException exception) { + OutputStream outputStream = new OutputStream() { + @Override + public void write(int i) throws IOException { + throw exception; + } + + @Override + public void close() throws IOException { + os.close(); + } + }; + + return outputStream; + } + + private void close(ServerSocket serverSocket) { + if (serverSocket.isClosed()) { + return; + } + + try { + serverSocket.close(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + private void close(Socket socket) { + if (socket.isClosed()) { + return; + } + + try { + socket.close(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + private void close(ScheduledExecutorService ses) { + if (ses.isTerminated()) { + return; + } + + try { + ses.shutdownNow(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + private Socket createSocket() throws IOException { + int port = getRandomFreePort(); + Socket socket = new Socket("localhost", port); + sockets.add(socket); + return socket; + } + + private ScheduledThreadPoolExecutor createSes(boolean removeOnCancelPolicy) { + ScheduledThreadPoolExecutor ses = createSes(); + ses.setRemoveOnCancelPolicy(removeOnCancelPolicy); + return ses; + } + + private ScheduledFuture getScheduledFeature( + TimeoutOutputStream timeoutOutputStream) { + try { + ScheduledFuture sf = + (ScheduledFuture) ReflectionUtil.getPrivateFieldValue(timeoutOutputStream, "sf"); + return sf; + } catch (Exception e) { + throw new RuntimeException("Couldn't extract scheduled feature", e); + } + } + + private ScheduledThreadPoolExecutor createSes() { + ScheduledThreadPoolExecutor ses = new ScheduledThreadPoolExecutor(1); + scheduledExecutorServices.add(ses); + return ses; + } + + private static final class CustomScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor { + private ScheduledFeatureMock scheduledFuture = new ScheduledFeatureMock(); + + public CustomScheduledThreadPoolExecutor(int corePoolSize) { + super(corePoolSize); + } + + @Override + public ScheduledFuture schedule(Callable callable, + long delay, TimeUnit unit) { + return (ScheduledFuture) scheduledFuture; + } + + public void setCancellationException(CancellationException cancellationException) { + scheduledFuture.setCancellationException(cancellationException); + } + + public void setInterruptedException(InterruptedException interruptedException) { + scheduledFuture.setInterruptedException(interruptedException); + } + + public void setExecutionException(ExecutionException executionException) { + scheduledFuture.setExecutionException(executionException); + } + + public void setTimeoutException(TimeoutException timeoutException) { + scheduledFuture.setTimeoutException(timeoutException); + } + + public void setRuntimeException(RuntimeException runtimeException) { + scheduledFuture.setRuntimeException(runtimeException); + } + } + + private static final class ScheduledFeatureMock implements ScheduledFuture { + private CancellationException cancellationException; + private InterruptedException interruptedException; + private ExecutionException executionException; + private TimeoutException timeoutException; + private RuntimeException runtimeException; + + @Override + public long getDelay(TimeUnit timeUnit) { + return 0; + } + + @Override + public int compareTo(Delayed delayed) { + return 0; + } + + @Override + public boolean cancel(boolean b) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return false; + } + + @Override + public String get() throws InterruptedException, ExecutionException { + return null; + } + + @Override + public String get(long l, TimeUnit timeUnit) throws InterruptedException, ExecutionException, TimeoutException { + if (cancellationException != null) { + throw cancellationException; + } + if (interruptedException != null) { + throw interruptedException; + } + + if (executionException != null) { + throw executionException; + } + + if (timeoutException != null) { + throw timeoutException; + } + + if (runtimeException != null) { + throw runtimeException; + } + + return "some result"; + } + + public void setCancellationException(CancellationException cancellationException) { + this.cancellationException = cancellationException; + } + + public void setInterruptedException(InterruptedException interruptedException) { + this.interruptedException = interruptedException; + } + + public void setExecutionException(ExecutionException executionException) { + this.executionException = executionException; + } + + public void setTimeoutException(TimeoutException timeoutException) { + this.timeoutException = timeoutException; + } + + public void setRuntimeException(RuntimeException runtimeException) { + this.runtimeException = runtimeException; + } + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/UUDecoderStreamTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/UUDecoderStreamTest.java new file mode 100644 index 0000000..570dc92 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/UUDecoderStreamTest.java @@ -0,0 +1,221 @@ +/* + * Copyright (c) 1997, 2023 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 org.xbib.net.mail.test.util; + +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Stream; +import org.junit.jupiter.params.provider.MethodSource; +import org.xbib.net.mail.util.UUDecoderStream; + +import java.io.BufferedReader; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStreamWriter; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + +import org.junit.jupiter.params.ParameterizedTest; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test uudecoder. + * + * @author Bill Shannon + */ +public class UUDecoderStreamTest { + + private static final Logger logger = Logger.getLogger(UUDecoderStreamTest.class.getName()); + + public UUDecoderStreamTest() { + } + + /** + * Test the data in the test case. + */ + @ParameterizedTest + @MethodSource("data") + void test(TestData t) { + InputStream in = new UUDecoderStream(new ByteArrayInputStream(t.input), + t.ignoreErrors, t.ignoreMissingBeginEnd); + + // two cases - either we're expecting an exception or we're not + if (t.expectedException != null) { + try { + int c; + while ((c = in.read()) >= 0) { + ; // throw it away + } + // read all the data with no exception - fail + fail("Didn't get expected exception: " + t.expectedException); + logger.log(Level.INFO, "Test: " + t.name + " got no Exception, expected Exception: " + + t.expectedException); + } catch (Exception ex) { + assertEquals(ex.getClass().getName(), t.expectedException); + if (!ex.getClass().getName().equals(t.expectedException)) { + logger.log(Level.INFO, "Test: " + t.name + " got Exception: " + ex + " expected Exception: " + + t.expectedException); + } + } finally { + try { + in.close(); + } catch (IOException ioex) { + // + } + } + } else { + InputStream ein = new ByteArrayInputStream(t.expectedOutput); + try { + int c, ec; + boolean gotError = false; + while ((c = in.read()) >= 0) { + ec = ein.read(); + if (ec < 0) { + logger.log(Level.INFO, "Test: " + t.name + " got char: " + c + " expected EOF"); + gotError = true; + break; + } + assertFalse(ec < 0); + assertEquals(ec, c); + if (c != ec) { + logger.log(Level.INFO, "Test: " + t.name + " got char: " + c + " expected char: " + ec); + gotError = true; + break; + } + } + if (!gotError) { + ec = ein.read(); + if (ec >= 0) { + logger.log(Level.INFO, "Test: " + t.name + " got EOF, expected char: " + ec); + } + assertFalse(ec >= 0); + } + } catch (Exception ex) { + logger.log(Level.INFO, "Test: " + t.name + " got Exception: " + ex + " expected no Exception"); + fail("Got exception: " + ex); + } finally { + try { + in.close(); + } catch (IOException ioex) { + logger.log(Level.WARNING, ioex.getMessage(), ioex); + } + try { + ein.close(); + } catch (IOException ioex) { + logger.log(Level.WARNING, ioex.getMessage(), ioex); + } + } + } + } + + private static Stream data() throws Exception { + // XXX - gratuitous array requirement + List testData = new ArrayList<>(); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader( + UUDecoderStreamTest.class.getResourceAsStream("uudata"))); + TestData t; + while ((t = parse(bufferedReader)) != null) { + testData.add(t); + } + return testData.stream(); + } + + /* + * Parse the input, returning a test case. + */ + private static TestData parse(BufferedReader in) throws Exception { + String line; + for (; ; ) { + line = in.readLine(); + if (line == null) + return null; + if (line.isEmpty() || line.startsWith("#")) + continue; + + if (!line.startsWith("TEST")) + throw new Exception("Bad test data format"); + break; + } + + TestData t = new TestData(); + int i = line.indexOf(' '); // XXX - crude + t.name = line.substring(i + 1); + + line = in.readLine(); + StringTokenizer st = new StringTokenizer(line); + String tok = st.nextToken(); + if (!tok.equals("DATA")) + throw new Exception("Bad test data format: " + line); + while (st.hasMoreTokens()) { + tok = st.nextToken(); + if (tok.equals("ignoreErrors")) + t.ignoreErrors = true; + else if (tok.equals("ignoreMissingBeginEnd")) + t.ignoreMissingBeginEnd = true; + else + throw new Exception("Bad DATA option in line: " + line); + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + Writer os = new OutputStreamWriter(bos, StandardCharsets.US_ASCII); + for (; ; ) { + line = in.readLine(); + if (line.equals("EXPECT")) + break; + os.write(line); + os.write("\n"); + } + os.close(); + t.input = bos.toByteArray(); + + bos = new ByteArrayOutputStream(); + os = new OutputStreamWriter(bos, StandardCharsets.US_ASCII); + for (; ; ) { + line = in.readLine(); + if (line.startsWith("EXCEPTION")) { + i = line.indexOf(' '); // XXX - crude + t.expectedException = line.substring(i + 1); + } else if (line.equals("END")) + break; + os.write(line); + os.write("\n"); + } + os.close(); + if (t.expectedException == null) { + t.expectedOutput = bos.toByteArray(); + } + return t; + } + + private static class TestData { + public String name; + public boolean ignoreErrors; + public boolean ignoreMissingBeginEnd; + public byte[] input; + public byte[] expectedOutput; + public String expectedException; + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/Utf8AddressTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/Utf8AddressTest.java new file mode 100644 index 0000000..aaf780b --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/Utf8AddressTest.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2010, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.mail.Message; +import jakarta.mail.Session; +import jakarta.mail.internet.InternetAddress; +import jakarta.mail.internet.MimeMessage; + +import java.util.Properties; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test "mail.mime.allowutf8" System property with address headers. + */ +public class Utf8AddressTest { + + private static Session s = Session.getInstance(new Properties()); + private static String utf8name = "test\u00a1\u00a2\u00a3"; + + @BeforeAll + public static void before() { + System.out.println("Utf8Address"); + s.getProperties().setProperty("mail.mime.allowutf8", "true"); + } + + @Test + public void testFrom() throws Exception { + MimeMessage m = new MimeMessage(s); + m.setFrom(new InternetAddress("joe@example.com", utf8name, "UTF-8")); + m.saveChanges(); + String h = m.getHeader("From", ""); + assertTrue(h.contains(utf8name)); + } + + @Test + public void testFromString() throws Exception { + MimeMessage m = new MimeMessage(s); + m.setFrom(utf8name + " "); + m.saveChanges(); + String h = m.getHeader("From", ""); + assertTrue(h.contains(utf8name)); + } + + @Test + public void testTo() throws Exception { + MimeMessage m = new MimeMessage(s); + m.setRecipient(Message.RecipientType.TO, + new InternetAddress("joe@example.com", utf8name, "UTF-8")); + m.saveChanges(); + String h = m.getHeader("To", ""); + assertTrue(h.contains(utf8name)); + } + + @Test + public void testToString() throws Exception { + MimeMessage m = new MimeMessage(s); + m.setRecipients(Message.RecipientType.TO, + utf8name + " "); + m.saveChanges(); + String h = m.getHeader("To", ""); + assertTrue(h.contains(utf8name)); + } + + @Test + public void testSender() throws Exception { + MimeMessage m = new MimeMessage(s); + m.setSender(new InternetAddress("joe@example.com", utf8name, "UTF-8")); + m.saveChanges(); + String h = m.getHeader("Sender", ""); + assertTrue(h.contains(utf8name)); + } + + @AfterAll + public static void after() { + // should be unnecessary + s.getProperties().remove("mail.mime.allowutf8"); + } +} diff --git a/net-mail/src/test/java/org/xbib/net/mail/test/util/WriteTimeoutSocketTest.java b/net-mail/src/test/java/org/xbib/net/mail/test/util/WriteTimeoutSocketTest.java new file mode 100644 index 0000000..7c79d59 --- /dev/null +++ b/net-mail/src/test/java/org/xbib/net/mail/test/util/WriteTimeoutSocketTest.java @@ -0,0 +1,536 @@ +/* + * Copyright (c) 2009, 2023 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 org.xbib.net.mail.test.util; + +import jakarta.activation.DataHandler; +import jakarta.mail.Folder; +import jakarta.mail.Message; +import jakarta.mail.MessagingException; +import jakarta.mail.Session; +import jakarta.mail.Store; +import jakarta.mail.StoreClosedException; +import jakarta.mail.internet.MimeMessage; +import jakarta.mail.util.ByteArrayDataSource; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.xbib.net.mail.iap.ConnectionException; +import org.xbib.net.mail.test.imap.IMAPHandler; +import org.xbib.net.mail.test.test.ReflectionUtil; +import org.xbib.net.mail.test.test.TestSSLSocketFactory; +import org.xbib.net.mail.test.test.TestServer; +import org.xbib.net.mail.test.test.TestSocketFactory; +import org.xbib.net.mail.util.WriteTimeoutSocket; + +import java.io.FileDescriptor; +import java.io.IOException; +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.concurrent.Callable; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test that write timeouts work. + */ +@Timeout(20) +public final class WriteTimeoutSocketTest { + private TestServer testServer; + private List scheduledExecutorServices = new ArrayList<>(); + private List writeTimeoutSockets = new ArrayList<>(); + + private static final int TIMEOUT = 200; // ms + private static final String data = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + @AfterEach + public void tearDown() { + close(testServer); + scheduledExecutorServices.forEach(this::close); + writeTimeoutSockets.forEach(this::close); + scheduledExecutorServices.clear(); + writeTimeoutSockets.clear(); + } + + /** + * Test write timeouts with plain sockets. + */ + @Test + public void test() throws Exception { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); + test(properties, false); + } + + /** + * Test write timeouts with custom socket factory. + */ + @Test + public void testSocketFactory() throws Exception { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); + TestSocketFactory sf = new TestSocketFactory(); + properties.put("mail.imap.socketFactory", sf); + properties.setProperty("mail.imap.socketFactory.fallback", "false"); + test(properties, false); + // make sure our socket factory was actually used + assertTrue(sf.getSocketCreated()); + } + + @Test //(expected = MessagingException.class) + public void testSSLCheckserveridentityDefaultsTrue() throws Exception { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); + properties.setProperty("mail.imap.ssl.enable", "true"); + properties.setProperty("mail.imap.ssl.trust", "localhost"); + test(properties, true); + } + + /** + * Test write timeouts with SSL sockets. + */ + @Test + public void testSSL() throws Exception { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.ssl.checkserveridentity", "false"); + properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); + properties.setProperty("mail.imap.ssl.enable", "true"); + properties.setProperty("mail.imap.ssl.trust", "localhost"); + test(properties, true); + } + + /** + * Test write timeouts with a custom SSL socket factory. + */ + @Test + public void testSSLSocketFactory() throws Exception { + final Properties properties = new Properties(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.ssl.checkserveridentity", "false"); + properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); + properties.setProperty("mail.imap.ssl.enable", "true"); + // TestSSLSocketFactory always trusts "localhost"; setting + // this property would cause MailSSLSocketFactory to be used instead + // of TestSSLSocketFactory, which we don't want. + //properties.setProperty("mail.imap.ssl.trust", "localhost"); + TestSSLSocketFactory sf = new TestSSLSocketFactory(); + properties.put("mail.imap.ssl.socketFactory", sf); + // don't fall back to non-SSL + properties.setProperty("mail.imap.socketFactory.fallback", "false"); + test(properties, true); + // make sure our socket factory was actually used + assertTrue(sf.getSocketWrapped() || sf.getSocketCreated()); + } + + /** + * Test that WriteTimeoutSocket overrides all methods from Socket. + * XXX - this is kind of hacky since it depends on Method.toString + */ + @Test + public void testOverrides() throws Exception { + Set socketMethods = new HashSet<>(); + Method[] m = Socket.class.getDeclaredMethods(); + String className = Socket.class.getName() + "."; + for (int i = 0; i < m.length; i++) { + if (Modifier.isPublic(m[i].getModifiers()) && + !Modifier.isStatic(m[i].getModifiers())) { + String name = m[i].toString(). + replace("synchronized ", ""). + replace(className, ""); + socketMethods.add(name); + } + } + Set wtsocketMethods = new HashSet<>(); + m = WriteTimeoutSocket.class.getDeclaredMethods(); + className = WriteTimeoutSocket.class.getName() + "."; + for (int i = 0; i < m.length; i++) { + if (Modifier.isPublic(m[i].getModifiers())) { + String name = m[i].toString(). + replace("synchronized ", ""). + replace(className, ""); + socketMethods.remove(name); + } + } + for (String s : socketMethods) + System.out.println("WriteTimeoutSocket did not override: " + s); + assertTrue(socketMethods.isEmpty()); + } + + private void test(Properties properties, boolean isSSL) throws Exception { + TestServer server = null; + try { + final TimeoutHandler handler = new TimeoutHandler(); + server = new TestServer(handler, isSSL); + server.start(); + + properties.setProperty("mail.imap.port", String.valueOf(server.getPort())); + final Session session = Session.getInstance(properties); + //session.setDebug(true); + + MimeMessage msg = new MimeMessage(session); + msg.setFrom("test@example.com"); + msg.setSubject("test"); + final int size = 8192000; // enough data to fill network buffers + byte[] part = new byte[size]; + for (int i = 0; i < size; i++) { + int j = i % 64; + if (j == 62) + part[i] = (byte) '\r'; + else if (j == 63) + part[i] = (byte) '\n'; + else + part[i] = (byte) data.charAt((j + i / 64) % 62); + } + msg.setDataHandler(new DataHandler( + new ByteArrayDataSource(part, "text/plain"))); + msg.saveChanges(); + + final Store store = session.getStore("imap"); + try { + store.connect("test", "test"); + final Folder f = store.getFolder("test"); + f.appendMessages(new Message[]{msg}); + fail("No timeout"); + } catch (StoreClosedException scex) { + // success! + } finally { + store.close(); + } + } finally { + if (server != null) { + server.quit(); + } + } + } + + @Test + public void testFileDescriptor$() throws Exception { + try (PublicFileSocket ps = new PublicFileSocket()) { + assertNotNull(ps.getFileDescriptor$()); + } + + testFileDescriptor$(new PublicFileSocket()); + testFileDescriptor$(new PublicFileSocket1of3()); + testFileDescriptor$(new PublicFileSocket2of3()); + testFileDescriptor$(new PublicFileSocket3of3()); + } + + @Test + public void testExternalSesIsBeingUsed() throws Exception { + final Properties properties = new Properties(); + CustomScheduledThreadPoolExecutor ses = new CustomScheduledThreadPoolExecutor(1); + scheduledExecutorServices.add(ses); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); + properties.put("mail.imap.executor.writetimeout", ses); + + test(properties, false); + + assertFalse(ses.isShutdownNowMethodCalled); + assertTrue(ses.isScheduleMethodCalled); + } + + @Test + public void testRejectedExecutionException() throws Exception { + final Properties properties = new Properties(); + CustomScheduledThreadPoolExecutor ses = new CustomScheduledThreadPoolExecutor(1); + scheduledExecutorServices.add(ses); + ses.shutdownNow(); + properties.setProperty("mail.imap.host", "localhost"); + properties.setProperty("mail.imap.writetimeout", "" + TIMEOUT); + properties.put("mail.imap.executor.writetimeout", ses); + + try { + test(properties, false); + fail("Expected IOException wasn't thrown "); + } catch (MessagingException mex) { + Throwable cause = mex.getCause(); + assertTrue(cause instanceof ConnectionException); + assertTrue(cause.getMessage().contains("java.io.IOException: Write aborted due to timeout not enforced")); + } + } + + @Test + public void testCloseOneSocketDoesntImpactAnother() throws Exception { + WriteTimeoutSocket wts1 = new WriteTimeoutSocket(new Socket(), 10000); + WriteTimeoutSocket wts2 = new WriteTimeoutSocket(new Socket(), 10000); + writeTimeoutSockets.add(wts1); + writeTimeoutSockets.add(wts2); + + ScheduledExecutorService ses1 = + (ScheduledExecutorService) ReflectionUtil.getPrivateFieldValue(wts1, "ses"); + ScheduledExecutorService ses2 = + (ScheduledExecutorService) ReflectionUtil.getPrivateFieldValue(wts2, "ses"); + scheduledExecutorServices.add(ses1); + scheduledExecutorServices.add(ses2); + + assertFalse(ses1.isTerminated()); + assertFalse(ses2.isTerminated()); + + wts1.close(); + assertTrue(ses1.isTerminated()); + assertFalse(ses2.isTerminated()); + } + + @Test + public void testDefaultSesConstructor1() throws Exception { + WriteTimeoutSocket writeTimeoutSocket = new WriteTimeoutSocket(new Socket(), 10000); + writeTimeoutSockets.add(writeTimeoutSocket); + + Object isExternalSes = ReflectionUtil.getPrivateFieldValue(writeTimeoutSocket, "isExternalSes"); + assertTrue(isExternalSes instanceof Boolean); + assertFalse((Boolean) isExternalSes); + } + + @Test + public void testDefaultSesConstructor2() throws Exception { + WriteTimeoutSocket writeTimeoutSocket = new WriteTimeoutSocket(10000); + writeTimeoutSockets.add(writeTimeoutSocket); + + Object isExternalSes = ReflectionUtil.getPrivateFieldValue(writeTimeoutSocket, "isExternalSes"); + assertTrue(isExternalSes instanceof Boolean); + assertFalse((Boolean) isExternalSes); + } + + @Test + public void testDefaultSesConstructor3() throws Exception { + testServer = getActiveTestServer(false); + + WriteTimeoutSocket writeTimeoutSocket = + new WriteTimeoutSocket("localhost", testServer.getPort(), 10000); + writeTimeoutSockets.add(writeTimeoutSocket); + + Object isExternalSes = ReflectionUtil.getPrivateFieldValue(writeTimeoutSocket, "isExternalSes"); + assertTrue(isExternalSes instanceof Boolean); + assertFalse((Boolean) isExternalSes); + } + + @Test + public void testDefaultSesConstructor4() throws Exception { + testServer = getActiveTestServer(false); + WriteTimeoutSocket writeTimeoutSocket = + new WriteTimeoutSocket(InetAddress.getLocalHost(), testServer.getPort(), 10000); + writeTimeoutSockets.add(writeTimeoutSocket); + + Object isExternalSes = ReflectionUtil.getPrivateFieldValue(writeTimeoutSocket, "isExternalSes"); + assertTrue(isExternalSes instanceof Boolean); + assertFalse((Boolean) isExternalSes); + } + + @Test + public void testDefaultSesConstructor5() throws Exception { + testServer = getActiveTestServer(false); + + WriteTimeoutSocket writeTimeoutSocket = + new WriteTimeoutSocket("localhost", testServer.getPort(), + (InetAddress) null, getRandomFreePort(), 10000); + writeTimeoutSockets.add(writeTimeoutSocket); + + Object isExternalSes = ReflectionUtil.getPrivateFieldValue(writeTimeoutSocket, "isExternalSes"); + assertTrue(isExternalSes instanceof Boolean); + assertFalse((Boolean) isExternalSes); + } + + @Test + public void testDefaultSesConstructor6() throws Exception { + testServer = getActiveTestServer(false); + + WriteTimeoutSocket writeTimeoutSocket = + new WriteTimeoutSocket(InetAddress.getByName("localhost"), testServer.getPort(), + (InetAddress) null, getRandomFreePort(), 10000); + writeTimeoutSockets.add(writeTimeoutSocket); + + Object isExternalSes = ReflectionUtil.getPrivateFieldValue(writeTimeoutSocket, "isExternalSes"); + assertTrue(isExternalSes instanceof Boolean); + assertFalse((Boolean) isExternalSes); + } + + @Test + public void testExternalSesConstructor7() throws Exception { + WriteTimeoutSocket writeTimeoutSocket = + new WriteTimeoutSocket(new Socket(), 10000, new ScheduledThreadPoolExecutor(1)); + writeTimeoutSockets.add(writeTimeoutSocket); + + Object isExternalSes = ReflectionUtil.getPrivateFieldValue(writeTimeoutSocket, "isExternalSes"); + assertTrue(isExternalSes instanceof Boolean); + assertTrue((Boolean) isExternalSes); + } + + @Test + public void testExternalSesOnClose() throws Exception { + Socket socket = new Socket(); + CustomScheduledThreadPoolExecutor ses = new CustomScheduledThreadPoolExecutor(1); + WriteTimeoutSocket writeTimeoutSocket = new WriteTimeoutSocket(socket, 10000, ses); + writeTimeoutSockets.add(writeTimeoutSocket); + writeTimeoutSocket.close(); + + assertFalse(ses.isShutdownNowMethodCalled); + } + + @Test + public void testDefaultSesOnClose() throws Exception { + Socket socket = new Socket(); + CustomScheduledThreadPoolExecutor ses = new CustomScheduledThreadPoolExecutor(1); + WriteTimeoutSocket writeTimeoutSocket = new WriteTimeoutSocket(socket, 10000); + writeTimeoutSockets.add(writeTimeoutSocket); + ReflectionUtil.setFieldValue(writeTimeoutSocket, "ses", ses); + + writeTimeoutSocket.close(); + + assertTrue(ses.isShutdownNowMethodCalled); + } + + private void testFileDescriptor$(Socket s) throws Exception { + try (WriteTimeoutSocket ws = new WriteTimeoutSocket(s, 1000)) { + assertNotNull(ws.getFileDescriptor$()); + } finally { + s.close(); + } + } + + private TestServer getActiveTestServer(boolean isSSL) { + TestServer server = null; + try { + final TimeoutHandler handler = new TimeoutHandler(); + server = new TestServer(handler, isSSL); + server.start(); + } catch (Exception e) { + throw new RuntimeException(e); + } + return server; + } + + private int getRandomFreePort() throws IOException { + ServerSocket serverSocket = new ServerSocket(0); + int freePort = serverSocket.getLocalPort(); + serverSocket.close(); + + return freePort; + } + + private void close(ScheduledExecutorService ses) { + if (ses.isTerminated()) { + return; + } + + try { + ses.shutdownNow(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + private void close(TestServer testServer) { + if (testServer == null || !testServer.isAlive()) { + return; + } + + try { + testServer.quit(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + private void close(WriteTimeoutSocket writeTimeoutSocket) { + if (writeTimeoutSocket.isClosed()) { + return; + } + + try { + writeTimeoutSocket.close(); + } catch (Exception e) { + System.out.println(e.getMessage()); + } + } + + private static class PublicFileSocket extends Socket { + public FileDescriptor getFileDescriptor$() { + return new FileDescriptor(); + } + } + + private static class PublicFileSocket1of3 extends PublicFileSocket { + } + + private static class PublicFileSocket2of3 extends PublicFileSocket1of3 { + } + + private static class PublicFileSocket3of3 extends PublicFileSocket2of3 { + } + + /** + * Custom handler. + */ + private static final class TimeoutHandler extends IMAPHandler { + @Override + protected void collectMessage(int bytes) throws IOException { + try { + // allow plenty of time for even slow machines to time out + Thread.sleep(TIMEOUT * 20); + } catch (InterruptedException ex) { + } + super.collectMessage(bytes); + } + + @Override + public void list(String line) throws IOException { + untagged("LIST () \"/\" test"); + ok(); + } + } + + private static final class CustomScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor { + private boolean isShutdownNowMethodCalled; + private boolean isScheduleMethodCalled; + + public CustomScheduledThreadPoolExecutor(int corePoolSize) { + super(corePoolSize); + } + + @Override + public ScheduledFuture schedule(Callable callable, + long delay, TimeUnit unit) { + isScheduleMethodCalled = true; + return super.schedule(callable, delay, unit); + } + + @Override + public List shutdownNow() { + isShutdownNowMethodCalled = true; + return super.shutdownNow(); + } + } +} diff --git a/net-mail/src/test/resources/META-INF/javamail.address.map b/net-mail/src/test/resources/META-INF/javamail.address.map new file mode 100644 index 0000000..4ab5572 --- /dev/null +++ b/net-mail/src/test/resources/META-INF/javamail.address.map @@ -0,0 +1 @@ +rfc822=smtp diff --git a/net-mail/src/test/resources/META-INF/javamail.charset.map b/net-mail/src/test/resources/META-INF/javamail.charset.map new file mode 100644 index 0000000..e933270 --- /dev/null +++ b/net-mail/src/test/resources/META-INF/javamail.charset.map @@ -0,0 +1,78 @@ +### JDK-to-MIME charset mapping table #### +### This should be the first mapping table ### + +8859_1 ISO-8859-1 +iso8859_1 ISO-8859-1 +ISO8859-1 ISO-8859-1 + +8859_2 ISO-8859-2 +iso8859_2 ISO-8859-2 +ISO8859-2 ISO-8859-2 + +8859_3 ISO-8859-3 +iso8859_3 ISO-8859-3 +ISO8859-3 ISO-8859-3 + +8859_4 ISO-8859-4 +iso8859_4 ISO-8859-4 +ISO8859-4 ISO-8859-4 + +8859_5 ISO-8859-5 +iso8859_5 ISO-8859-5 +ISO8859-5 ISO-8859-5 + +8859_6 ISO-8859-6 +iso8859_6 ISO-8859-6 +ISO8859-6 ISO-8859-6 + +8859_7 ISO-8859-7 +iso8859_7 ISO-8859-7 +ISO8859-7 ISO-8859-7 + +8859_8 ISO-8859-8 +iso8859_8 ISO-8859-8 +ISO8859-8 ISO-8859-8 + +8859_9 ISO-8859-9 +iso8859_9 ISO-8859-9 +ISO8859-9 ISO-8859-9 + +SJIS Shift_JIS +JIS ISO-2022-JP +ISO2022JP ISO-2022-JP +EUC_JP euc-jp +KOI8_R koi8-r +EUC_CN euc-cn +EUC_TW euc-tw +EUC_KR euc-kr + +--DIVIDER: this line *must* start with "--" and end with "--" -- + +#### XXX-to-JDK charset mapping table #### + +iso-2022-cn ISO2022CN +iso-2022-kr ISO2022KR +utf-8 UTF8 +utf8 UTF8 +en_US.iso885915 ISO-8859-15 +ja_jp.iso2022-7 ISO2022JP +ja_jp.eucjp EUCJIS + +# these two are not needed in 1.1.6. (since EUC_KR exists +# and KSC5601 will map to the correct converter) +euc-kr KSC5601 +euckr KSC5601 + +# in JDK 1.1.6 we will no longer need the "us-ascii" convert +us-ascii ISO-8859-1 +x-us-ascii ISO-8859-1 + +# Chinese charsets are a mess and widely misrepresented. +# gb18030 is a superset of gbk, which is a supserset of cp936/ms936, +# which is a superset of gb2312. +# https://bugzilla.gnome.org/show_bug.cgi?id=446783 +# map all of these to gb18030. +gb2312 GB18030 +cp936 GB18030 +ms936 GB18030 +gbk GB18030 diff --git a/net-mail/src/test/resources/META-INF/javamail.default.address.map b/net-mail/src/test/resources/META-INF/javamail.default.address.map new file mode 100644 index 0000000..4ab5572 --- /dev/null +++ b/net-mail/src/test/resources/META-INF/javamail.default.address.map @@ -0,0 +1 @@ +rfc822=smtp diff --git a/net-mail/src/test/resources/META-INF/javamail.providers b/net-mail/src/test/resources/META-INF/javamail.providers new file mode 100644 index 0000000..cf2148b --- /dev/null +++ b/net-mail/src/test/resources/META-INF/javamail.providers @@ -0,0 +1,4 @@ +protocol=pop3; type=store; class=org.xbib.net.mail.pop3.POP3Store; vendor=xbib; +protocol=pop3s; type=store; class=org.xbib.net.mail.pop3.POP3SSLStore; vendor=xbib; +protocol=smtp; type=transport; class=org.xbib.net.mail.smtp.SMTPTransport; vendor=xbib; +protocol=smtps; type=transport; class=org.xbib.net.mail.smtp.SMTPSSLTransport; vendor=xbib; diff --git a/net-mail/src/test/resources/META-INF/mailcap b/net-mail/src/test/resources/META-INF/mailcap new file mode 100644 index 0000000..16e58a3 --- /dev/null +++ b/net-mail/src/test/resources/META-INF/mailcap @@ -0,0 +1,5 @@ +text/plain;; x-java-content-handler=org.xbib.net.mail.handlers.text_plain +text/html;; x-java-content-handler=org.xbib.net.mail.handlers.text_html +text/xml;; x-java-content-handler=org.xbib.net.mail.handlers.text_xml +multipart/*;; x-java-content-handler=org.xbib.net.mail.handlers.multipart_mixed; x-java-fallback-entry=true +message/rfc822;; x-java-content-handler=org.xbib.net.mail.handlers.message_rfc822 diff --git a/net-mail/src/test/resources/logging.properties b/net-mail/src/test/resources/logging.properties new file mode 100644 index 0000000..b5665ac --- /dev/null +++ b/net-mail/src/test/resources/logging.properties @@ -0,0 +1,4 @@ +handlers=java.util.logging.ConsoleHandler +.level=INFO +java.util.logging.ConsoleHandler.level=INFO +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter diff --git a/net-mail/src/test/resources/org/xbib/net/mail/test/imap/protocol/uiddata b/net-mail/src/test/resources/org/xbib/net/mail/test/imap/protocol/uiddata new file mode 100644 index 0000000..d3784f5 --- /dev/null +++ b/net-mail/src/test/resources/org/xbib/net/mail/test/imap/protocol/uiddata @@ -0,0 +1,77 @@ +# +# Copyright (c) 1997, 2023 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 +# + + +# +# Data to test UIDSet. +# + +TEST one UID +DATA 1 +EXPECT 1 + +TEST two UIDs +DATA 1,3 +EXPECT 1 3 + +TEST UID range +DATA 1:2 +EXPECT 1 2 + +TEST bigger UID range +DATA 1:3 +EXPECT 1 2 3 + +TEST two ranges +DATA 1:3,5:7 +EXPECT 1 2 3 5 6 7 + +TEST ranges and singles +DATA 1:3,5,7:9 +EXPECT 1 2 3 5 7 8 9 + +TEST many singles +DATA 1,3,5,7,9 +EXPECT 1 3 5 7 9 + +TEST max +DATA 1 +MAX 1 +EXPECT 1 + +TEST max2 +DATA 2 +MAX 2 +EXPECT 2 + +TEST max3 +DATA 1:2 +MAX 2 +EXPECT 1 2 + +TEST max4 +DATA 1:2 +MAX 1 1 +EXPECT 1 + +TEST max5 +DATA 1:4 +MAX 3 1:3 +EXPECT 1 2 3 + +TEST empty +DATA EMPTY +EXPECT EMPTY diff --git a/net-mail/src/test/resources/org/xbib/net/mail/test/test/keystore.jks b/net-mail/src/test/resources/org/xbib/net/mail/test/test/keystore.jks new file mode 100644 index 0000000000000000000000000000000000000000..94d61f64cc39b74cf375708489f8af2e2ea56c81 GIT binary patch literal 2246 zcmchYdo>x#XH8_e=PdYea0MESHu>ntKwFuu_vP`cecqq##qC892n600@LRA5KT?Dm z$&Yx^kGRXu9Y@02Kp^N2o&w#%BX|W=cmXJY1q%ZJ4+u(uepH*imIrf9n(Bs#leufS zR7Se0$J)5UJ_oM}B8_j1fJ(_F*;Qv0i%faL*E>UEEu*ohWTs_pCo^BX!4qPd;@Y+_ z%ET#a$5-Y`u-^)`Ch>o0f&{4TT$c)pvd7M6EEr!NbY2%6jyaDo)xc{%Q>@+5t{9)R z$g?7$1!g+!J}Tb{ghNfJOzYXExTGAOLY7fEeW>5#j=be9Zib6#7*@X_>q)b6T$#sy zs#|vB_U39|am7YfhLo9 zLlsn6>hiuib&aBpLurnlrFT>uzF0KV>M(LsK3BH~I&bt)y~b@eS1vBO7qSYR*VDnZedJWnhBrxIr@yO=HP zVJh{>rE9#bk+HM9jD(ZCTYb~AP`cjh@52&$Cd-kJelp&BD|8@#Ww!Kc51)dpZrwyX zF1vkpVYojRNj~e+qe3s+4p*EoMCQLvb;$vMU(+)E-{e{VqSn_rXiYz9wo-g?@Oz?O zx#qY!tCq&<{7R(04W7THABL^#W`L9ut25vME+@N9(MqVmdTm>xaPHY(`e7;#@V3bA zpuo)GPxnq!1uAboi@~)as&Kh0r@4Ra`G;Tx=nTad_vffW%Fsdv9^1BT-N(; z({s}M49wFCc-{tzCvE@&Ho=c9L%b7i9L1!ddtC^Z1Hh6qi9!meoDYS`wC*q?C5~jF zzFcme3+*^sa?}+#Nbx7jtksBwyEKu0C|yRp_@viI*Z=!H#wWLoxSA}jrCD)&2_Exm znVVlEup#xF!40`T)9yw!xpd6SUO3AZWY@+7#kV$fqR^>_t{z^{^N~dG^j>m6V8mM-JmP9OS+<|A+V1|_Yv8Ooj8zSZ@ z>ss5HbB5YOQOO-x_dX5NvDZV;>xlchhX+%v!m7U4%qtiH4VMx(o@6pN&^eG1oTp4; z*6AyB0kIY1F3vhSP{?mUTqVU8ORi{qV5Jh{?{o6kpm2TuD3N+Lx2NId;farK?Th5# z)Tz~dmZ@(nbF6vkjZ3UDn{$I#^2jQW`xu~{fQJJD=^{VR(~x`B?G7+LdyFL6a8W2@ z--WfGBy}|ydaM)iO-P)MRuo7#*f}99d@!2k#x_Gfi>W)J7-Bqfv~20nn;V*v!*rKLuTN!^fIGO6%g<9eSkQoupj6MF-x_MBR-I`l#FWX@%X*WzEFK zOXvn)`HUeQgW`Uusw9WBww#!OQuxFs7=i@``9?1`H9XW+ZCgj+ zDhM0ls7VPUQw#*!@)a*FNDk$3NMGT($B*NXu|jETv4p}J^O`7`yd;BeV?tbI^ChbT z!J`=&*wEQ2cGK+jqRzode*)yW$aIHtL+gfhbA^|bP0C2o=)Cg>~b*&2UN%DB@Gf;rwh{U1!0?MBjBIN@LY$zyqI^!F!J!wTLjOzl z{6YBsAOe37*dGM)KY~Z?gA;V{n)pK+nuoMC3GTnGHeTaD_Wyhh1yJ~B9J{XqLIDIp zcnTl@rT_qFbg4Mtojv7>ABTyEe}m*lms^gaUx|Tu(MbP!_*GkI;ETIomdRPTVZr#! zDvzrdt_hnb=qpf)6vI|c%DhSS!_|9s;7;Fenw072i?!WQKV`dZAo}pH>7-R@gnykI ztRQhVzCQa)O^CcqSWsid;jM#`EGxv3M{YiL>{_IQi2NS*T8Y01j258f=^J1^wf^pL zf9UYWOD483(u>poZXw@(B6X}1P>F;^iZ9{zuW87 zRdeOR5C8;}$m93pWp^7Qi-1Z%#o8%Fb8BhH@^se1$GnyWYg*7=#easl+o|0U0T3*z zK6=Rc%5s@&1kRTrpHN~Q!5NU{7h~PdCUUoqNX8p}f>)UiRA2@sIjCYM&7iJ84z;r3 z0rbp!rD>q6eMN3I@(!=Ev3^|BY))ans``Zkx2X-c-W)^eh`9%u#JkdC4@WaTa5Bvg z4P{piyI@%+&DEeKs%-_za#nF0v7lA;&N(@cx_AD@hY1ZQkX`-`l5RFNbZvm;m0f@XGUk;DMCN z()V8W$uX2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST no begin +DATA +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +EXCEPTION org.xbib.net.mail.util.DecodingException +END + +TEST no end +DATA +begin 644 encoder.buf +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +EXPECT +EXCEPTION org.xbib.net.mail.util.DecodingException +END + +TEST no end, no empty line +DATA +begin 644 encoder.buf +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! +EXPECT +EXCEPTION org.xbib.net.mail.util.DecodingException +END + +TEST no begin, ignore errors +DATA ignoreMissingBeginEnd +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST no end, ignore errors +DATA ignoreMissingBeginEnd +begin 644 encoder.buf +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST no begin, no end, ignore errors +DATA ignoreMissingBeginEnd +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST empty line, ignore errors +DATA ignoreMissingBeginEnd + +begin 644 encoder.buf +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST empty line, no begin, ignore errors +DATA ignoreMissingBeginEnd + +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST bad mode +DATA +begin xxx encoder.buf +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +EXCEPTION org.xbib.net.mail.util.DecodingException +END + +TEST bad mode, ignore errors +DATA ignoreErrors +begin xxx encoder.buf +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST bad filename +DATA +begin 644 +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +EXCEPTION org.xbib.net.mail.util.DecodingException +END + +TEST bad filename, ignore errors +DATA ignoreErrors +begin 644 +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST garbage data +DATA +begin 644 encoder.buf +XXX +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +EXCEPTION org.xbib.net.mail.util.DecodingException +END + +TEST garbage data (tab) +DATA +begin 644 encoder.buf + +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +EXCEPTION org.xbib.net.mail.util.DecodingException +END + +TEST garbage data, ignore errors +DATA ignoreErrors +begin 644 encoder.buf +XXX +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! + +end +EXPECT +this is a very very very very very long line to test the decoder +END + +TEST ignore both kinds of errors +DATA ignoreErrors ignoreMissingBeginEnd +XXX +M=&AI2!V97)Y('9E2!L;VYG(&QI;F4@ +4=&\@=&5S="!T:&4@9&5C;V1E<@H! +EXPECT +this is a very very very very very long line to test the decoder +END diff --git a/net-mime/src/main/java/org/xbib/net/mime/stream/Base64.java b/net-mime/src/main/java/org/xbib/net/mime/stream/Base64.java index 239325a..fa9f2b9 100644 --- a/net-mime/src/main/java/org/xbib/net/mime/stream/Base64.java +++ b/net-mime/src/main/java/org/xbib/net/mime/stream/Base64.java @@ -178,7 +178,9 @@ public class Base64 { } encoder.output = new byte[output_len]; encoder.process(input, offset, len, true); - assert encoder.op == output_len; + if (encoder.op != output_len) { + throw new IllegalStateException(); + } return encoder.output; } @@ -614,8 +616,6 @@ public class Base64 { if (do_cr) output[op++] = '\r'; output[op++] = '\n'; } - assert tailLen == 0; - assert p == len; } else { // Save the leftovers in tail to be consumed on the next // call to encodeInternal. diff --git a/net-security-auth/src/main/java/module-info.java b/net-security-auth/src/main/java/module-info.java new file mode 100644 index 0000000..a542eb3 --- /dev/null +++ b/net-security-auth/src/main/java/module-info.java @@ -0,0 +1,5 @@ +module org.xbib.net.security.auth { + requires java.logging; + requires java.security.sasl; + exports org.xbib.net.security.auth; +} diff --git a/net-security-auth/src/main/java/org/xbib/net/security/auth/MD4.java b/net-security-auth/src/main/java/org/xbib/net/security/auth/MD4.java new file mode 100644 index 0000000..e0f8e4c --- /dev/null +++ b/net-security-auth/src/main/java/org/xbib/net/security/auth/MD4.java @@ -0,0 +1,272 @@ +/* + * Copyright (c) 2005, 2023 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 org.xbib.net.security.auth; + +/** + * 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/net-security-auth/src/main/java/org/xbib/net/security/auth/Ntlm.java b/net-security-auth/src/main/java/org/xbib/net/security/auth/Ntlm.java new file mode 100644 index 0000000..49f2d0e --- /dev/null +++ b/net-security-auth/src/main/java/org/xbib/net/security/auth/Ntlm.java @@ -0,0 +1,471 @@ +/* + * Copyright (c) 2005, 2024 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 org.xbib.net.security.auth; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Locale; +import java.util.Random; +import java.util.logging.Level; +import java.util.logging.Logger; +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; + + +/** + * NTLMAuthentication: + * + * @author Michael McMahon + * @author Bill Shannon (adapted for Jakarta Mail) + */ +public class Ntlm { + + private static final Logger logger = Logger.getLogger(Ntlm.class.getName()); + + private byte[] type1; + private byte[] type3; + + private SecretKeyFactory fac; + private Cipher cipher; + private MD4 md4; + private final String hostname; + private final String ntdomain; + private final String username; + private final String password; + + private Mac hmac; + + // 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 | NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } + + ; + + /** + * 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 + */ + public Ntlm(String ntdomain, String hostname, String username, + String password) { + 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; + 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) { + throw new AssertionError(e); + } + } + + // 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 = new String(Base64.getEncoder().encode(msg), + StandardCharsets.ISO_8859_1); + 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(ex); + } + try { + byte[] nk = new byte[16]; + System.arraycopy(key, 0, nk, 0, Math.min(key.length, 16)); + SecretKeySpec skey = new SecretKeySpec(nk, "HmacMD5"); + hmac.init(skey); + return hmac.doFinal(text); + } catch (InvalidKeyException | RuntimeException ex) { + throw new AssertionError(ex); + } + } + + private byte[] calcLMHash() throws GeneralSecurityException { + byte[] magic = {0x4b, 0x47, 0x53, 0x21, 0x40, 0x23, 0x24, 0x25}; + byte[] pwb = password.toUpperCase(Locale.ENGLISH).getBytes( + StandardCharsets.ISO_8859_1); + 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) { + throw new AssertionError(e); + } + 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 { + if (key.length != 21) { + throw new AssertionError(); + } + 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) { + throw new AssertionError(ex); + } + 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 = Base64.getDecoder().decode( + type2msg.getBytes(StandardCharsets.US_ASCII)); + 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; + + int flags = readInt(type2, 20); + byte[] lmresponse; + byte[] ntresponse; + + // 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); + writeInt(type3, 60, type3flags); + + byte[] msg = new byte[l]; + System.arraycopy(type3, 0, msg, 0, l); + + if (logger.isLoggable(Level.FINE)) + logger.fine("type 3 message: " + toHex(msg)); + + String result = new String(Base64.getEncoder().encode(msg), + StandardCharsets.ISO_8859_1); + 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 final 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/net-security-auth/src/main/java/org/xbib/net/security/auth/OAuth2SaslClient.java b/net-security-auth/src/main/java/org/xbib/net/security/auth/OAuth2SaslClient.java new file mode 100644 index 0000000..b3f82ef --- /dev/null +++ b/net-security-auth/src/main/java/org/xbib/net/security/auth/OAuth2SaslClient.java @@ -0,0 +1,116 @@ +/* + * Copyright (c) 2014, 2023 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 org.xbib.net.security.auth; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Map; +import javax.security.auth.callback.Callback; +import javax.security.auth.callback.CallbackHandler; +import javax.security.auth.callback.NameCallback; +import javax.security.auth.callback.PasswordCallback; +import javax.security.auth.callback.UnsupportedCallbackException; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslException; + +/** + * Jakarta Mail SASL client for OAUTH2. + * + * @author Bill Shannon + * @see + * RFC 6749 - OAuth 2.0 Authorization Framework + * @see + * RFC 6750 - OAuth 2.0 Authorization Framework: Bearer Token Usage + */ +public class OAuth2SaslClient implements SaslClient { + private CallbackHandler cbh; + //private Map props; // XXX - not currently used + private boolean complete = false; + + public OAuth2SaslClient(Map props, CallbackHandler cbh) { + //this.props = props; + this.cbh = cbh; + } + + @Override + public String getMechanismName() { + return "XOAUTH2"; + } + + @Override + public boolean hasInitialResponse() { + return true; + } + + @Override + public byte[] evaluateChallenge(byte[] challenge) throws SaslException { + if (complete) + return new byte[0]; + + NameCallback ncb = new NameCallback("User name:"); + PasswordCallback pcb = new PasswordCallback("OAuth token:", false); + try { + cbh.handle(new Callback[]{ncb, pcb}); + } catch (UnsupportedCallbackException ex) { + throw new SaslException("Unsupported callback", ex); + } catch (IOException ex) { + throw new SaslException("Callback handler failed", ex); + } + + /* + * The OAuth token isn't really a password, and Jakarta Mail doesn't + * use char[] for passwords, so we don't worry about storing the + * token in strings. + */ + String user = ncb.getName(); + String token = new String(pcb.getPassword()); + pcb.clearPassword(); + String resp = "user=" + user + "\001auth=Bearer " + token + "\001\001"; + byte[] response; + response = resp.getBytes(StandardCharsets.UTF_8); + complete = true; + return response; + } + + @Override + public boolean isComplete() { + return complete; + } + + @Override + public byte[] unwrap(byte[] incoming, int offset, int len) + throws SaslException { + throw new IllegalStateException("OAUTH2 unwrap not supported"); + } + + @Override + public byte[] wrap(byte[] outgoing, int offset, int len) + throws SaslException { + throw new IllegalStateException("OAUTH2 wrap not supported"); + } + + @Override + public Object getNegotiatedProperty(String propName) { + if (!complete) + throw new IllegalStateException("OAUTH2 getNegotiatedProperty"); + return null; + } + + @Override + public void dispose() throws SaslException { + } +} diff --git a/net-security-auth/src/main/java/org/xbib/net/security/auth/OAuth2SaslClientFactory.java b/net-security-auth/src/main/java/org/xbib/net/security/auth/OAuth2SaslClientFactory.java new file mode 100644 index 0000000..e1d2c92 --- /dev/null +++ b/net-security-auth/src/main/java/org/xbib/net/security/auth/OAuth2SaslClientFactory.java @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2014, 2023 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 org.xbib.net.security.auth; + +import java.security.Provider; +import java.security.Security; +import java.util.Map; +import javax.security.auth.callback.CallbackHandler; +import javax.security.sasl.SaslClient; +import javax.security.sasl.SaslClientFactory; +import javax.security.sasl.SaslException; + +/** + * Jakarta Mail SASL client factory for OAUTH2. + * + * @author Bill Shannon + */ +public class OAuth2SaslClientFactory implements SaslClientFactory { + + private static final String PROVIDER_NAME = "Jakarta-Mail-OAuth2"; + private static final String MECHANISM_NAME = "SaslClientFactory.XOAUTH2"; + + static class OAuth2Provider extends Provider { + + public OAuth2Provider() { + super(PROVIDER_NAME, "1.0", "XOAUTH2 SASL Mechanism"); + put(MECHANISM_NAME, OAuth2SaslClientFactory.class.getName()); + } + } + + /** + * Creates a default {@code OAuth2SaslClientFactory}. + * + * @see #init() + */ + public OAuth2SaslClientFactory() { + } + + @Override + public SaslClient createSaslClient(String[] mechanisms, + String authorizationId, String protocol, + String serverName, Map props, + CallbackHandler cbh) throws SaslException { + for (String m : mechanisms) { + if (m.equals("XOAUTH2")) + return new OAuth2SaslClient(props, cbh); + } + return null; + } + + @Override + public String[] getMechanismNames(Map props) { + return new String[]{"XOAUTH2"}; + } + + /** + * Initialize this OAUTH2 provider, but only if there isn't one already. + * If we're not allowed to add this provider, just give up silently. + */ + public static void init() { + try { + if (Security.getProvider(PROVIDER_NAME) == null) + Security.addProvider(new OAuth2Provider()); + } catch (SecurityException ex) { + // oh well... + } + } +} diff --git a/net-security-auth/src/main/java/org/xbib/net/security/auth/package-info.java b/net-security-auth/src/main/java/org/xbib/net/security/auth/package-info.java new file mode 100644 index 0000000..d9bbe36 --- /dev/null +++ b/net-security-auth/src/main/java/org/xbib/net/security/auth/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2021 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 + */ + +/** + * This package includes internal authentication support classes and + * SHOULD NOT BE USED DIRECTLY BY APPLICATIONS. + */ +package org.xbib.net.security.auth; diff --git a/settings.gradle b/settings.gradle index 9bf4c78..29a377f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,9 +34,11 @@ dependencyResolutionManagement { include 'net' include 'net-bouncycastle' +include 'net-mail' include 'net-mime' include 'net-path' include 'net-resource' include 'net-security' +include 'net-security-auth' include 'net-socket' include 'benchmark'